Repository: mindoc-org/mindoc Branch: master Commit: bec17630274e Files: 1184 Total size: 46.9 MB Directory structure: gitextract_8kp_8qo8/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE │ └── workflows/ │ └── build.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.md ├── README.md ├── appveyor.yml ├── build_amd64.sh ├── build_musl_amd64.sh ├── cache/ │ ├── cache.go │ └── cache_null.go ├── commands/ │ ├── command.go │ ├── daemon/ │ │ └── daemon.go │ ├── install.go │ ├── migrate/ │ │ ├── migrate.go │ │ └── migrate_v03.go │ └── update.go ├── conf/ │ ├── app.conf.example │ ├── enumerate.go │ ├── lang/ │ │ ├── en-us.ini │ │ ├── ru-ru.ini │ │ └── zh-cn.ini │ ├── mail.go │ └── workweixin.go ├── controllers/ │ ├── AccountController.go │ ├── BaseController.go │ ├── BlogController.go │ ├── BookController.go │ ├── BookMemberController.go │ ├── CommentController.go │ ├── DocumentController.go │ ├── ErrorController.go │ ├── HomeController.go │ ├── ItemsetsController.go │ ├── LabelController.go │ ├── ManagerController.go │ ├── SearchController.go │ ├── SettingController.go │ ├── TemplateController.go │ └── const.go ├── converter/ │ ├── converter.go │ └── util.go ├── database/ │ └── clean.py ├── dev-win-build.cmd ├── docker-compose.yml ├── go.mod ├── go.sum ├── graphics/ │ ├── copy.go │ └── file.go ├── lib/ │ └── time/ │ ├── README │ └── update.bash ├── mail/ │ ├── smtp.go │ ├── smtp_test.go │ └── util.go ├── main.go ├── mcp/ │ ├── handler.go │ ├── mcp.go │ └── middleware.go ├── models/ │ ├── AttachmentModel.go │ ├── AttachmentResult.go │ ├── Auth2Account.go │ ├── Base.go │ ├── Blog.go │ ├── BlogResult.go │ ├── BookModel.go │ ├── BookResult.go │ ├── CommentModel.go │ ├── ContentReverseIndex.go │ ├── ConvertBookResult.go │ ├── Dashboard.go │ ├── DocumentHistory.go │ ├── DocumentModel.go │ ├── DocumentSearchResult.go │ ├── DocumentTree.go │ ├── Errors.go │ ├── Itemsets.go │ ├── LabelModel.go │ ├── Logs.go │ ├── Member.go │ ├── MemberResult.go │ ├── MemberToken.go │ ├── Migrations.go │ ├── Options.go │ ├── Relationship.go │ ├── Team.go │ ├── TeamMember.go │ ├── TeamRelationship.go │ ├── Template.go │ ├── comment_result.go │ └── comment_vote.go ├── routers/ │ ├── filter.go │ └── router.go ├── simsun.ttc ├── start.sh ├── static/ │ ├── bootstrap/ │ │ ├── css/ │ │ │ ├── bootstrap-theme.css │ │ │ └── bootstrap.css │ │ ├── js/ │ │ │ ├── bootstrap.js │ │ │ └── npm.js │ │ └── plugins/ │ │ ├── bootstrap-fileinput/ │ │ │ └── 4.4.7/ │ │ │ ├── css/ │ │ │ │ ├── fileinput-rtl.css │ │ │ │ └── fileinput.css │ │ │ ├── js/ │ │ │ │ ├── fileinput.js │ │ │ │ ├── locales/ │ │ │ │ │ ├── LANG.js │ │ │ │ │ ├── ar.js │ │ │ │ │ ├── az.js │ │ │ │ │ ├── bg.js │ │ │ │ │ ├── ca.js │ │ │ │ │ ├── cr.js │ │ │ │ │ ├── cs.js │ │ │ │ │ ├── da.js │ │ │ │ │ ├── de.js │ │ │ │ │ ├── el.js │ │ │ │ │ ├── es.js │ │ │ │ │ ├── et.js │ │ │ │ │ ├── fa.js │ │ │ │ │ ├── fi.js │ │ │ │ │ ├── fr.js │ │ │ │ │ ├── gl.js │ │ │ │ │ ├── hu.js │ │ │ │ │ ├── id.js │ │ │ │ │ ├── it.js │ │ │ │ │ ├── ja.js │ │ │ │ │ ├── ka.js │ │ │ │ │ ├── kr.js │ │ │ │ │ ├── kz.js │ │ │ │ │ ├── lt.js │ │ │ │ │ ├── nl.js │ │ │ │ │ ├── no.js │ │ │ │ │ ├── pl.js │ │ │ │ │ ├── pt-BR.js │ │ │ │ │ ├── pt.js │ │ │ │ │ ├── ro.js │ │ │ │ │ ├── ru.js │ │ │ │ │ ├── sk.js │ │ │ │ │ ├── sl.js │ │ │ │ │ ├── sv.js │ │ │ │ │ ├── th.js │ │ │ │ │ ├── tr.js │ │ │ │ │ ├── uk.js │ │ │ │ │ ├── vi.js │ │ │ │ │ ├── zh-TW.js │ │ │ │ │ └── zh.js │ │ │ │ └── plugins/ │ │ │ │ ├── piexif.js │ │ │ │ ├── purify.js │ │ │ │ └── sortable.js │ │ │ └── themes/ │ │ │ ├── explorer/ │ │ │ │ ├── theme.css │ │ │ │ └── theme.js │ │ │ ├── explorer-fa/ │ │ │ │ ├── theme.css │ │ │ │ └── theme.js │ │ │ ├── fa/ │ │ │ │ └── theme.js │ │ │ └── gly/ │ │ │ └── theme.js │ │ ├── bootstrap-switch/ │ │ │ ├── css/ │ │ │ │ ├── bootstrap2/ │ │ │ │ │ └── bootstrap-switch.css │ │ │ │ └── bootstrap3/ │ │ │ │ └── bootstrap-switch.css │ │ │ └── js/ │ │ │ └── bootstrap-switch.js │ │ ├── bootstrap-wysiwyg/ │ │ │ ├── bootstrap-wysiwyg.js │ │ │ └── external/ │ │ │ ├── google-code-prettify/ │ │ │ │ ├── lang-apollo.js │ │ │ │ ├── lang-basic.js │ │ │ │ ├── lang-clj.js │ │ │ │ ├── lang-css.js │ │ │ │ ├── lang-dart.js │ │ │ │ ├── lang-erlang.js │ │ │ │ ├── lang-go.js │ │ │ │ ├── lang-hs.js │ │ │ │ ├── lang-lisp.js │ │ │ │ ├── lang-llvm.js │ │ │ │ ├── lang-lua.js │ │ │ │ ├── lang-matlab.js │ │ │ │ ├── lang-ml.js │ │ │ │ ├── lang-mumps.js │ │ │ │ ├── lang-n.js │ │ │ │ ├── lang-pascal.js │ │ │ │ ├── lang-proto.js │ │ │ │ ├── lang-r.js │ │ │ │ ├── lang-rd.js │ │ │ │ ├── lang-scala.js │ │ │ │ ├── lang-sql.js │ │ │ │ ├── lang-tcl.js │ │ │ │ ├── lang-tex.js │ │ │ │ ├── lang-vb.js │ │ │ │ ├── lang-vhdl.js │ │ │ │ ├── lang-wiki.js │ │ │ │ ├── lang-xq.js │ │ │ │ ├── lang-yaml.js │ │ │ │ ├── prettify.css │ │ │ │ ├── prettify.js │ │ │ │ └── run_prettify.js │ │ │ └── jquery.hotkeys.js │ │ └── tagsinput/ │ │ ├── bootstrap-tagsinput.css │ │ ├── bootstrap-tagsinput.js │ │ └── bootstrap-tagsinput.less │ ├── bootstrap-paginator/ │ │ └── bootstrap-paginator.js │ ├── cherry/ │ │ ├── addons/ │ │ │ ├── cherry-code-block-mermaid-plugin.d.ts │ │ │ ├── cherry-code-block-mermaid-plugin.js │ │ │ ├── cherry-code-block-plantuml-plugin.d.ts │ │ │ └── cherry-code-block-plantuml-plugin.js │ │ ├── cherry-markdown.css │ │ ├── cherry-markdown.js │ │ ├── drawio-demo.js │ │ ├── drawio_demo/ │ │ │ ├── Actions.js │ │ │ ├── Dialogs.js │ │ │ ├── Editor.js │ │ │ ├── EditorUi.js │ │ │ ├── Format.js │ │ │ ├── Graph.js │ │ │ ├── Init.js │ │ │ ├── Menus.js │ │ │ ├── Shapes.js │ │ │ ├── Sidebar.js │ │ │ ├── Toolbar.js │ │ │ ├── atlas.css │ │ │ ├── dark-default.xml │ │ │ ├── dark.css │ │ │ ├── default-old.xml │ │ │ ├── default.xml │ │ │ ├── drawio-demo.js │ │ │ ├── font/ │ │ │ │ └── graph.iconfont.less │ │ │ ├── grapheditor.css │ │ │ ├── image/ │ │ │ │ └── stencils/ │ │ │ │ ├── arrows.xml │ │ │ │ ├── basic.xml │ │ │ │ ├── bpmn.xml │ │ │ │ └── flowchart.xml │ │ │ ├── jscolor/ │ │ │ │ └── jscolor.js │ │ │ ├── lib/ │ │ │ │ └── base64.js │ │ │ ├── resources/ │ │ │ │ ├── en.txt │ │ │ │ └── zh.txt │ │ │ └── src/ │ │ │ ├── css/ │ │ │ │ ├── common.css │ │ │ │ └── explorer.css │ │ │ ├── grapheditor.less │ │ │ ├── js/ │ │ │ │ ├── editor/ │ │ │ │ │ ├── mxDefaultKeyHandler.js │ │ │ │ │ ├── mxDefaultPopupMenu.js │ │ │ │ │ ├── mxDefaultToolbar.js │ │ │ │ │ └── mxEditor.js │ │ │ │ ├── handler/ │ │ │ │ │ ├── mxCellHighlight.js │ │ │ │ │ ├── mxCellMarker.js │ │ │ │ │ ├── mxCellTracker.js │ │ │ │ │ ├── mxConnectionHandler.js │ │ │ │ │ ├── mxConstraintHandler.js │ │ │ │ │ ├── mxEdgeHandler.js │ │ │ │ │ ├── mxEdgeSegmentHandler.js │ │ │ │ │ ├── mxElbowEdgeHandler.js │ │ │ │ │ ├── mxGraphHandler.js │ │ │ │ │ ├── mxHandle.js │ │ │ │ │ ├── mxKeyHandler.js │ │ │ │ │ ├── mxPanningHandler.js │ │ │ │ │ ├── mxPopupMenuHandler.js │ │ │ │ │ ├── mxRubberband.js │ │ │ │ │ ├── mxSelectionCellsHandler.js │ │ │ │ │ ├── mxTooltipHandler.js │ │ │ │ │ └── mxVertexHandler.js │ │ │ │ ├── index.txt │ │ │ │ ├── io/ │ │ │ │ │ ├── mxCellCodec.js │ │ │ │ │ ├── mxChildChangeCodec.js │ │ │ │ │ ├── mxCodec.js │ │ │ │ │ ├── mxCodecRegistry.js │ │ │ │ │ ├── mxDefaultKeyHandlerCodec.js │ │ │ │ │ ├── mxDefaultPopupMenuCodec.js │ │ │ │ │ ├── mxDefaultToolbarCodec.js │ │ │ │ │ ├── mxEditorCodec.js │ │ │ │ │ ├── mxGenericChangeCodec.js │ │ │ │ │ ├── mxGraphCodec.js │ │ │ │ │ ├── mxGraphViewCodec.js │ │ │ │ │ ├── mxModelCodec.js │ │ │ │ │ ├── mxObjectCodec.js │ │ │ │ │ ├── mxRootChangeCodec.js │ │ │ │ │ ├── mxStylesheetCodec.js │ │ │ │ │ └── mxTerminalChangeCodec.js │ │ │ │ ├── layout/ │ │ │ │ │ ├── hierarchical/ │ │ │ │ │ │ ├── model/ │ │ │ │ │ │ │ ├── mxGraphAbstractHierarchyCell.js │ │ │ │ │ │ │ ├── mxGraphHierarchyEdge.js │ │ │ │ │ │ │ ├── mxGraphHierarchyModel.js │ │ │ │ │ │ │ ├── mxGraphHierarchyNode.js │ │ │ │ │ │ │ └── mxSwimlaneModel.js │ │ │ │ │ │ ├── mxHierarchicalLayout.js │ │ │ │ │ │ ├── mxSwimlaneLayout.js │ │ │ │ │ │ └── stage/ │ │ │ │ │ │ ├── mxCoordinateAssignment.js │ │ │ │ │ │ ├── mxHierarchicalLayoutStage.js │ │ │ │ │ │ ├── mxMedianHybridCrossingReduction.js │ │ │ │ │ │ ├── mxMinimumCycleRemover.js │ │ │ │ │ │ └── mxSwimlaneOrdering.js │ │ │ │ │ ├── mxCircleLayout.js │ │ │ │ │ ├── mxCompactTreeLayout.js │ │ │ │ │ ├── mxCompositeLayout.js │ │ │ │ │ ├── mxEdgeLabelLayout.js │ │ │ │ │ ├── mxFastOrganicLayout.js │ │ │ │ │ ├── mxGraphLayout.js │ │ │ │ │ ├── mxParallelEdgeLayout.js │ │ │ │ │ ├── mxPartitionLayout.js │ │ │ │ │ ├── mxRadialTreeLayout.js │ │ │ │ │ └── mxStackLayout.js │ │ │ │ ├── model/ │ │ │ │ │ ├── mxCell.js │ │ │ │ │ ├── mxCellPath.js │ │ │ │ │ ├── mxGeometry.js │ │ │ │ │ └── mxGraphModel.js │ │ │ │ ├── mxClient.js │ │ │ │ ├── shape/ │ │ │ │ │ ├── mxActor.js │ │ │ │ │ ├── mxArrow.js │ │ │ │ │ ├── mxArrowConnector.js │ │ │ │ │ ├── mxCloud.js │ │ │ │ │ ├── mxConnector.js │ │ │ │ │ ├── mxCylinder.js │ │ │ │ │ ├── mxDoubleEllipse.js │ │ │ │ │ ├── mxEllipse.js │ │ │ │ │ ├── mxHexagon.js │ │ │ │ │ ├── mxImageShape.js │ │ │ │ │ ├── mxLabel.js │ │ │ │ │ ├── mxLine.js │ │ │ │ │ ├── mxMarker.js │ │ │ │ │ ├── mxPolyline.js │ │ │ │ │ ├── mxRectangleShape.js │ │ │ │ │ ├── mxRhombus.js │ │ │ │ │ ├── mxShape.js │ │ │ │ │ ├── mxStencil.js │ │ │ │ │ ├── mxStencilRegistry.js │ │ │ │ │ ├── mxSwimlane.js │ │ │ │ │ ├── mxText.js │ │ │ │ │ └── mxTriangle.js │ │ │ │ ├── util/ │ │ │ │ │ ├── mxAbstractCanvas2D.js │ │ │ │ │ ├── mxAnimation.js │ │ │ │ │ ├── mxAutoSaveManager.js │ │ │ │ │ ├── mxClipboard.js │ │ │ │ │ ├── mxConstants.js │ │ │ │ │ ├── mxDictionary.js │ │ │ │ │ ├── mxDivResizer.js │ │ │ │ │ ├── mxDragSource.js │ │ │ │ │ ├── mxEffects.js │ │ │ │ │ ├── mxEvent.js │ │ │ │ │ ├── mxEventObject.js │ │ │ │ │ ├── mxEventSource.js │ │ │ │ │ ├── mxForm.js │ │ │ │ │ ├── mxGuide.js │ │ │ │ │ ├── mxImage.js │ │ │ │ │ ├── mxImageBundle.js │ │ │ │ │ ├── mxImageExport.js │ │ │ │ │ ├── mxLog.js │ │ │ │ │ ├── mxMorphing.js │ │ │ │ │ ├── mxMouseEvent.js │ │ │ │ │ ├── mxObjectIdentity.js │ │ │ │ │ ├── mxPanningManager.js │ │ │ │ │ ├── mxPoint.js │ │ │ │ │ ├── mxPopupMenu.js │ │ │ │ │ ├── mxRectangle.js │ │ │ │ │ ├── mxResources.js │ │ │ │ │ ├── mxSvgCanvas2D.js │ │ │ │ │ ├── mxToolbar.js │ │ │ │ │ ├── mxUndoManager.js │ │ │ │ │ ├── mxUndoableEdit.js │ │ │ │ │ ├── mxUrlConverter.js │ │ │ │ │ ├── mxUtils.js │ │ │ │ │ ├── mxVmlCanvas2D.js │ │ │ │ │ ├── mxWindow.js │ │ │ │ │ ├── mxXmlCanvas2D.js │ │ │ │ │ └── mxXmlRequest.js │ │ │ │ └── view/ │ │ │ │ ├── mxCellEditor.js │ │ │ │ ├── mxCellOverlay.js │ │ │ │ ├── mxCellRenderer.js │ │ │ │ ├── mxCellState.js │ │ │ │ ├── mxCellStatePreview.js │ │ │ │ ├── mxConnectionConstraint.js │ │ │ │ ├── mxEdgeStyle.js │ │ │ │ ├── mxGraph.js │ │ │ │ ├── mxGraphSelectionModel.js │ │ │ │ ├── mxGraphView.js │ │ │ │ ├── mxLayoutManager.js │ │ │ │ ├── mxMultiplicity.js │ │ │ │ ├── mxOutline.js │ │ │ │ ├── mxPerimeter.js │ │ │ │ ├── mxPrintPreview.js │ │ │ │ ├── mxStyleRegistry.js │ │ │ │ ├── mxStylesheet.js │ │ │ │ ├── mxSwimlaneManager.js │ │ │ │ └── mxTemporaryCellStates.js │ │ │ └── resources/ │ │ │ ├── editor.txt │ │ │ ├── editor_de.txt │ │ │ ├── editor_zh.txt │ │ │ ├── graph.txt │ │ │ ├── graph_de.txt │ │ │ └── graph_zh.txt │ │ ├── drawio_demo.html │ │ ├── mxgraph/ │ │ │ ├── css/ │ │ │ │ ├── common.css │ │ │ │ └── explorer.css │ │ │ └── mxClient.js │ │ └── pinyin/ │ │ ├── README.md │ │ ├── hanziPinyin.js │ │ ├── hanziPinyinWithoutYin.js │ │ ├── pinyin.js │ │ └── pinyin_dist.js │ ├── cropper/ │ │ └── 2.3.4/ │ │ ├── cropper.css │ │ └── cropper.js │ ├── css/ │ │ ├── export.css │ │ ├── jstree.css │ │ ├── kancloud.css │ │ ├── main.css │ │ ├── markdown.css │ │ ├── markdown.preview.css │ │ └── print.css │ ├── editor.md/ │ │ ├── css/ │ │ │ ├── editormd.css │ │ │ ├── editormd.logo.css │ │ │ └── editormd.preview.css │ │ ├── editormd.amd.js │ │ ├── editormd.js │ │ ├── fonts/ │ │ │ └── FontAwesome.otf │ │ ├── languages/ │ │ │ ├── en.js │ │ │ └── zh-tw.js │ │ ├── lib/ │ │ │ ├── codemirror/ │ │ │ │ ├── AUTHORS │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── addon/ │ │ │ │ │ ├── comment/ │ │ │ │ │ │ ├── comment.js │ │ │ │ │ │ └── continuecomment.js │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── dialog.css │ │ │ │ │ │ └── dialog.js │ │ │ │ │ ├── display/ │ │ │ │ │ │ ├── fullscreen.css │ │ │ │ │ │ ├── fullscreen.js │ │ │ │ │ │ ├── panel.js │ │ │ │ │ │ ├── placeholder.js │ │ │ │ │ │ └── rulers.js │ │ │ │ │ ├── edit/ │ │ │ │ │ │ ├── closebrackets.js │ │ │ │ │ │ ├── closetag.js │ │ │ │ │ │ ├── continuelist.js │ │ │ │ │ │ ├── matchbrackets.js │ │ │ │ │ │ ├── matchtags.js │ │ │ │ │ │ └── trailingspace.js │ │ │ │ │ ├── fold/ │ │ │ │ │ │ ├── brace-fold.js │ │ │ │ │ │ ├── comment-fold.js │ │ │ │ │ │ ├── foldcode.js │ │ │ │ │ │ ├── foldgutter.css │ │ │ │ │ │ ├── foldgutter.js │ │ │ │ │ │ ├── indent-fold.js │ │ │ │ │ │ ├── markdown-fold.js │ │ │ │ │ │ └── xml-fold.js │ │ │ │ │ ├── hint/ │ │ │ │ │ │ ├── anyword-hint.js │ │ │ │ │ │ ├── css-hint.js │ │ │ │ │ │ ├── html-hint.js │ │ │ │ │ │ ├── javascript-hint.js │ │ │ │ │ │ ├── show-hint.css │ │ │ │ │ │ ├── show-hint.js │ │ │ │ │ │ ├── sql-hint.js │ │ │ │ │ │ └── xml-hint.js │ │ │ │ │ ├── lint/ │ │ │ │ │ │ ├── coffeescript-lint.js │ │ │ │ │ │ ├── css-lint.js │ │ │ │ │ │ ├── javascript-lint.js │ │ │ │ │ │ ├── json-lint.js │ │ │ │ │ │ ├── lint.css │ │ │ │ │ │ ├── lint.js │ │ │ │ │ │ └── yaml-lint.js │ │ │ │ │ ├── merge/ │ │ │ │ │ │ ├── merge.css │ │ │ │ │ │ └── merge.js │ │ │ │ │ ├── mode/ │ │ │ │ │ │ ├── loadmode.js │ │ │ │ │ │ ├── multiplex.js │ │ │ │ │ │ ├── multiplex_test.js │ │ │ │ │ │ ├── overlay.js │ │ │ │ │ │ └── simple.js │ │ │ │ │ ├── runmode/ │ │ │ │ │ │ ├── colorize.js │ │ │ │ │ │ ├── runmode-standalone.js │ │ │ │ │ │ ├── runmode.js │ │ │ │ │ │ └── runmode.node.js │ │ │ │ │ ├── scroll/ │ │ │ │ │ │ ├── annotatescrollbar.js │ │ │ │ │ │ ├── scrollpastend.js │ │ │ │ │ │ ├── simplescrollbars.css │ │ │ │ │ │ └── simplescrollbars.js │ │ │ │ │ ├── search/ │ │ │ │ │ │ ├── match-highlighter.js │ │ │ │ │ │ ├── matchesonscrollbar.css │ │ │ │ │ │ ├── matchesonscrollbar.js │ │ │ │ │ │ ├── search.js │ │ │ │ │ │ └── searchcursor.js │ │ │ │ │ ├── selection/ │ │ │ │ │ │ ├── active-line.js │ │ │ │ │ │ ├── mark-selection.js │ │ │ │ │ │ └── selection-pointer.js │ │ │ │ │ ├── tern/ │ │ │ │ │ │ ├── tern.css │ │ │ │ │ │ ├── tern.js │ │ │ │ │ │ └── worker.js │ │ │ │ │ └── wrap/ │ │ │ │ │ └── hardwrap.js │ │ │ │ ├── bower.json │ │ │ │ ├── lib/ │ │ │ │ │ ├── codemirror.css │ │ │ │ │ └── codemirror.js │ │ │ │ ├── mode/ │ │ │ │ │ ├── apl/ │ │ │ │ │ │ ├── apl.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── asterisk/ │ │ │ │ │ │ ├── asterisk.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── clike/ │ │ │ │ │ │ ├── clike.js │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── scala.html │ │ │ │ │ ├── clojure/ │ │ │ │ │ │ ├── clojure.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── cobol/ │ │ │ │ │ │ ├── cobol.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── coffeescript/ │ │ │ │ │ │ ├── coffeescript.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── commonlisp/ │ │ │ │ │ │ ├── commonlisp.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── css/ │ │ │ │ │ │ ├── css.js │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── less.html │ │ │ │ │ │ ├── less_test.js │ │ │ │ │ │ ├── scss.html │ │ │ │ │ │ ├── scss_test.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── cypher/ │ │ │ │ │ │ ├── cypher.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── d/ │ │ │ │ │ │ ├── d.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── dart/ │ │ │ │ │ │ ├── dart.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── diff/ │ │ │ │ │ │ ├── diff.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── django/ │ │ │ │ │ │ ├── django.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── dockerfile/ │ │ │ │ │ │ ├── dockerfile.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── dtd/ │ │ │ │ │ │ ├── dtd.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── dylan/ │ │ │ │ │ │ ├── dylan.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── ebnf/ │ │ │ │ │ │ ├── ebnf.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── ecl/ │ │ │ │ │ │ ├── ecl.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── eiffel/ │ │ │ │ │ │ ├── eiffel.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── erlang/ │ │ │ │ │ │ ├── erlang.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── forth/ │ │ │ │ │ │ ├── forth.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── fortran/ │ │ │ │ │ │ ├── fortran.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── gas/ │ │ │ │ │ │ ├── gas.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── gfm/ │ │ │ │ │ │ ├── gfm.js │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── gherkin/ │ │ │ │ │ │ ├── gherkin.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── go/ │ │ │ │ │ │ ├── go.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── groovy/ │ │ │ │ │ │ ├── groovy.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── haml/ │ │ │ │ │ │ ├── haml.js │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── haskell/ │ │ │ │ │ │ ├── haskell.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── haxe/ │ │ │ │ │ │ ├── haxe.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── htmlembedded/ │ │ │ │ │ │ ├── htmlembedded.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── htmlmixed/ │ │ │ │ │ │ ├── htmlmixed.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── http/ │ │ │ │ │ │ ├── http.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── idl/ │ │ │ │ │ │ ├── idl.js │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── index.html │ │ │ │ │ ├── jade/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── jade.js │ │ │ │ │ ├── javascript/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── javascript.js │ │ │ │ │ │ ├── json-ld.html │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── typescript.html │ │ │ │ │ ├── jinja2/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── jinja2.js │ │ │ │ │ ├── julia/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── julia.js │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── kotlin.js │ │ │ │ │ ├── livescript/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── livescript.js │ │ │ │ │ ├── lua/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── lua.js │ │ │ │ │ ├── markdown/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── markdown.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── meta.js │ │ │ │ │ ├── mirc/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── mirc.js │ │ │ │ │ ├── mllike/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── mllike.js │ │ │ │ │ ├── modelica/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── modelica.js │ │ │ │ │ ├── nginx/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── nginx.js │ │ │ │ │ ├── ntriples/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── ntriples.js │ │ │ │ │ ├── octave/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── octave.js │ │ │ │ │ ├── pascal/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── pascal.js │ │ │ │ │ ├── pegjs/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── pegjs.js │ │ │ │ │ ├── perl/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── perl.js │ │ │ │ │ ├── php/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── php.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── pig/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── pig.js │ │ │ │ │ ├── properties/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── properties.js │ │ │ │ │ ├── puppet/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── puppet.js │ │ │ │ │ ├── python/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── python.js │ │ │ │ │ ├── q/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── q.js │ │ │ │ │ ├── r/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── r.js │ │ │ │ │ ├── rpm/ │ │ │ │ │ │ ├── changes/ │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── rpm.js │ │ │ │ │ ├── rst/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── rst.js │ │ │ │ │ ├── ruby/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── ruby.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── rust/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── rust.js │ │ │ │ │ ├── sass/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── sass.js │ │ │ │ │ ├── scheme/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── scheme.js │ │ │ │ │ ├── shell/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── shell.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── sieve/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── sieve.js │ │ │ │ │ ├── slim/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── slim.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── smalltalk/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── smalltalk.js │ │ │ │ │ ├── smarty/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── smarty.js │ │ │ │ │ ├── smartymixed/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── smartymixed.js │ │ │ │ │ ├── solr/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── solr.js │ │ │ │ │ ├── soy/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── soy.js │ │ │ │ │ ├── sparql/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── sparql.js │ │ │ │ │ ├── spreadsheet/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── spreadsheet.js │ │ │ │ │ ├── sql/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── sql.js │ │ │ │ │ ├── stex/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── stex.js │ │ │ │ │ │ └── test.js │ │ │ │ │ ├── stylus/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── stylus.js │ │ │ │ │ ├── tcl/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── tcl.js │ │ │ │ │ ├── textile/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── textile.js │ │ │ │ │ ├── tiddlywiki/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── tiddlywiki.css │ │ │ │ │ │ └── tiddlywiki.js │ │ │ │ │ ├── tiki/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── tiki.css │ │ │ │ │ │ └── tiki.js │ │ │ │ │ ├── toml/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── toml.js │ │ │ │ │ ├── tornado/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── tornado.js │ │ │ │ │ ├── turtle/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── turtle.js │ │ │ │ │ ├── vb/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── vb.js │ │ │ │ │ ├── vbscript/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── vbscript.js │ │ │ │ │ ├── velocity/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── velocity.js │ │ │ │ │ ├── verilog/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── verilog.js │ │ │ │ │ ├── xml/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── xml.js │ │ │ │ │ ├── xquery/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── test.js │ │ │ │ │ │ └── xquery.js │ │ │ │ │ ├── yaml/ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ └── yaml.js │ │ │ │ │ └── z80/ │ │ │ │ │ ├── index.html │ │ │ │ │ └── z80.js │ │ │ │ ├── package.json │ │ │ │ └── theme/ │ │ │ │ ├── 3024-day.css │ │ │ │ ├── 3024-night.css │ │ │ │ ├── ambiance-mobile.css │ │ │ │ ├── ambiance.css │ │ │ │ ├── base16-dark.css │ │ │ │ ├── base16-light.css │ │ │ │ ├── blackboard.css │ │ │ │ ├── cobalt.css │ │ │ │ ├── colorforth.css │ │ │ │ ├── eclipse.css │ │ │ │ ├── elegant.css │ │ │ │ ├── erlang-dark.css │ │ │ │ ├── lesser-dark.css │ │ │ │ ├── mbo.css │ │ │ │ ├── mdn-like.css │ │ │ │ ├── midnight.css │ │ │ │ ├── monokai.css │ │ │ │ ├── neat.css │ │ │ │ ├── neo.css │ │ │ │ ├── night.css │ │ │ │ ├── paraiso-dark.css │ │ │ │ ├── paraiso-light.css │ │ │ │ ├── pastel-on-dark.css │ │ │ │ ├── rubyblue.css │ │ │ │ ├── solarized.css │ │ │ │ ├── the-matrix.css │ │ │ │ ├── tomorrow-night-bright.css │ │ │ │ ├── tomorrow-night-eighties.css │ │ │ │ ├── twilight.css │ │ │ │ ├── vibrant-ink.css │ │ │ │ ├── xq-dark.css │ │ │ │ ├── xq-light.css │ │ │ │ └── zenburn.css │ │ │ ├── highlight/ │ │ │ │ ├── highlight.js │ │ │ │ ├── languages/ │ │ │ │ │ ├── 1c.js │ │ │ │ │ ├── abnf.js │ │ │ │ │ ├── accesslog.js │ │ │ │ │ ├── actionscript.js │ │ │ │ │ ├── ada.js │ │ │ │ │ ├── apache.js │ │ │ │ │ ├── applescript.js │ │ │ │ │ ├── arduino.js │ │ │ │ │ ├── armasm.js │ │ │ │ │ ├── asciidoc.js │ │ │ │ │ ├── aspectj.js │ │ │ │ │ ├── autohotkey.js │ │ │ │ │ ├── autoit.js │ │ │ │ │ ├── avrasm.js │ │ │ │ │ ├── awk.js │ │ │ │ │ ├── axapta.js │ │ │ │ │ ├── bash.js │ │ │ │ │ ├── basic.js │ │ │ │ │ ├── bnf.js │ │ │ │ │ ├── brainfuck.js │ │ │ │ │ ├── cal.js │ │ │ │ │ ├── capnproto.js │ │ │ │ │ ├── ceylon.js │ │ │ │ │ ├── clean.js │ │ │ │ │ ├── clojure-repl.js │ │ │ │ │ ├── clojure.js │ │ │ │ │ ├── cmake.js │ │ │ │ │ ├── coffeescript.js │ │ │ │ │ ├── coq.js │ │ │ │ │ ├── cos.js │ │ │ │ │ ├── cpp.js │ │ │ │ │ ├── crmsh.js │ │ │ │ │ ├── crystal.js │ │ │ │ │ ├── cs.js │ │ │ │ │ ├── csp.js │ │ │ │ │ ├── css.js │ │ │ │ │ ├── d.js │ │ │ │ │ ├── dart.js │ │ │ │ │ ├── delphi.js │ │ │ │ │ ├── diff.js │ │ │ │ │ ├── django.js │ │ │ │ │ ├── dns.js │ │ │ │ │ ├── dockerfile.js │ │ │ │ │ ├── dos.js │ │ │ │ │ ├── dsconfig.js │ │ │ │ │ ├── dts.js │ │ │ │ │ ├── dust.js │ │ │ │ │ ├── ebnf.js │ │ │ │ │ ├── elixir.js │ │ │ │ │ ├── elm.js │ │ │ │ │ ├── erb.js │ │ │ │ │ ├── erlang-repl.js │ │ │ │ │ ├── erlang.js │ │ │ │ │ ├── excel.js │ │ │ │ │ ├── fix.js │ │ │ │ │ ├── flix.js │ │ │ │ │ ├── fortran.js │ │ │ │ │ ├── fsharp.js │ │ │ │ │ ├── gams.js │ │ │ │ │ ├── gauss.js │ │ │ │ │ ├── gcode.js │ │ │ │ │ ├── gherkin.js │ │ │ │ │ ├── glsl.js │ │ │ │ │ ├── go.js │ │ │ │ │ ├── golo.js │ │ │ │ │ ├── gradle.js │ │ │ │ │ ├── groovy.js │ │ │ │ │ ├── haml.js │ │ │ │ │ ├── handlebars.js │ │ │ │ │ ├── haskell.js │ │ │ │ │ ├── haxe.js │ │ │ │ │ ├── hsp.js │ │ │ │ │ ├── htmlbars.js │ │ │ │ │ ├── http.js │ │ │ │ │ ├── inform7.js │ │ │ │ │ ├── ini.js │ │ │ │ │ ├── irpf90.js │ │ │ │ │ ├── java.js │ │ │ │ │ ├── javascript.js │ │ │ │ │ ├── json.js │ │ │ │ │ ├── julia.js │ │ │ │ │ ├── kotlin.js │ │ │ │ │ ├── lasso.js │ │ │ │ │ ├── ldif.js │ │ │ │ │ ├── less.js │ │ │ │ │ ├── lisp.js │ │ │ │ │ ├── livecodeserver.js │ │ │ │ │ ├── livescript.js │ │ │ │ │ ├── lsl.js │ │ │ │ │ ├── lua.js │ │ │ │ │ ├── makefile.js │ │ │ │ │ ├── markdown.js │ │ │ │ │ ├── mathematica.js │ │ │ │ │ ├── matlab.js │ │ │ │ │ ├── maxima.js │ │ │ │ │ ├── mel.js │ │ │ │ │ ├── mercury.js │ │ │ │ │ ├── mipsasm.js │ │ │ │ │ ├── mizar.js │ │ │ │ │ ├── mojolicious.js │ │ │ │ │ ├── monkey.js │ │ │ │ │ ├── moonscript.js │ │ │ │ │ ├── nginx.js │ │ │ │ │ ├── nimrod.js │ │ │ │ │ ├── nix.js │ │ │ │ │ ├── nsis.js │ │ │ │ │ ├── objectivec.js │ │ │ │ │ ├── ocaml.js │ │ │ │ │ ├── openscad.js │ │ │ │ │ ├── oxygene.js │ │ │ │ │ ├── parser3.js │ │ │ │ │ ├── perl.js │ │ │ │ │ ├── pf.js │ │ │ │ │ ├── php.js │ │ │ │ │ ├── pony.js │ │ │ │ │ ├── powershell.js │ │ │ │ │ ├── processing.js │ │ │ │ │ ├── profile.js │ │ │ │ │ ├── prolog.js │ │ │ │ │ ├── protobuf.js │ │ │ │ │ ├── puppet.js │ │ │ │ │ ├── purebasic.js │ │ │ │ │ ├── python.js │ │ │ │ │ ├── q.js │ │ │ │ │ ├── qml.js │ │ │ │ │ ├── r.js │ │ │ │ │ ├── rib.js │ │ │ │ │ ├── roboconf.js │ │ │ │ │ ├── rsl.js │ │ │ │ │ ├── ruby.js │ │ │ │ │ ├── ruleslanguage.js │ │ │ │ │ ├── rust.js │ │ │ │ │ ├── scala.js │ │ │ │ │ ├── scheme.js │ │ │ │ │ ├── scilab.js │ │ │ │ │ ├── scss.js │ │ │ │ │ ├── smali.js │ │ │ │ │ ├── smalltalk.js │ │ │ │ │ ├── sml.js │ │ │ │ │ ├── sqf.js │ │ │ │ │ ├── sql.js │ │ │ │ │ ├── stan.js │ │ │ │ │ ├── stata.js │ │ │ │ │ ├── step21.js │ │ │ │ │ ├── stylus.js │ │ │ │ │ ├── subunit.js │ │ │ │ │ ├── swift.js │ │ │ │ │ ├── taggerscript.js │ │ │ │ │ ├── tap.js │ │ │ │ │ ├── tcl.js │ │ │ │ │ ├── tex.js │ │ │ │ │ ├── thrift.js │ │ │ │ │ ├── tp.js │ │ │ │ │ ├── twig.js │ │ │ │ │ ├── typescript.js │ │ │ │ │ ├── vala.js │ │ │ │ │ ├── vbnet.js │ │ │ │ │ ├── vbscript-html.js │ │ │ │ │ ├── vbscript.js │ │ │ │ │ ├── verilog.js │ │ │ │ │ ├── vhdl.js │ │ │ │ │ ├── vim.js │ │ │ │ │ ├── x86asm.js │ │ │ │ │ ├── xl.js │ │ │ │ │ ├── xml.js │ │ │ │ │ ├── xquery.js │ │ │ │ │ ├── yaml.js │ │ │ │ │ └── zephir.js │ │ │ │ └── styles/ │ │ │ │ ├── agate.css │ │ │ │ ├── androidstudio.css │ │ │ │ ├── arduino-light.css │ │ │ │ ├── arta.css │ │ │ │ ├── ascetic.css │ │ │ │ ├── atelier-cave-dark.css │ │ │ │ ├── atelier-cave-light.css │ │ │ │ ├── atelier-dune-dark.css │ │ │ │ ├── atelier-dune-light.css │ │ │ │ ├── atelier-estuary-dark.css │ │ │ │ ├── atelier-estuary-light.css │ │ │ │ ├── atelier-forest-dark.css │ │ │ │ ├── atelier-forest-light.css │ │ │ │ ├── atelier-heath-dark.css │ │ │ │ ├── atelier-heath-light.css │ │ │ │ ├── atelier-lakeside-dark.css │ │ │ │ ├── atelier-lakeside-light.css │ │ │ │ ├── atelier-plateau-dark.css │ │ │ │ ├── atelier-plateau-light.css │ │ │ │ ├── atelier-savanna-dark.css │ │ │ │ ├── atelier-savanna-light.css │ │ │ │ ├── atelier-seaside-dark.css │ │ │ │ ├── atelier-seaside-light.css │ │ │ │ ├── atelier-sulphurpool-dark.css │ │ │ │ ├── atelier-sulphurpool-light.css │ │ │ │ ├── atom-one-dark.css │ │ │ │ ├── atom-one-light.css │ │ │ │ ├── brown-paper.css │ │ │ │ ├── codepen-embed.css │ │ │ │ ├── color-brewer.css │ │ │ │ ├── darcula.css │ │ │ │ ├── dark.css │ │ │ │ ├── darkula.css │ │ │ │ ├── default.css │ │ │ │ ├── docco.css │ │ │ │ ├── dracula.css │ │ │ │ ├── far.css │ │ │ │ ├── foundation.css │ │ │ │ ├── github-gist.css │ │ │ │ ├── github.css │ │ │ │ ├── googlecode.css │ │ │ │ ├── grayscale.css │ │ │ │ ├── gruvbox-dark.css │ │ │ │ ├── gruvbox-light.css │ │ │ │ ├── hopscotch.css │ │ │ │ ├── hybrid.css │ │ │ │ ├── idea.css │ │ │ │ ├── ir-black.css │ │ │ │ ├── kimbie.dark.css │ │ │ │ ├── kimbie.light.css │ │ │ │ ├── magula.css │ │ │ │ ├── mono-blue.css │ │ │ │ ├── monokai-sublime.css │ │ │ │ ├── monokai.css │ │ │ │ ├── obsidian.css │ │ │ │ ├── ocean.css │ │ │ │ ├── paraiso-dark.css │ │ │ │ ├── paraiso-light.css │ │ │ │ ├── pojoaque.css │ │ │ │ ├── purebasic.css │ │ │ │ ├── qtcreator_dark.css │ │ │ │ ├── qtcreator_light.css │ │ │ │ ├── railscasts.css │ │ │ │ ├── rainbow.css │ │ │ │ ├── school-book.css │ │ │ │ ├── solarized-dark.css │ │ │ │ ├── solarized-light.css │ │ │ │ ├── sunburst.css │ │ │ │ ├── tomorrow-night-blue.css │ │ │ │ ├── tomorrow-night-bright.css │ │ │ │ ├── tomorrow-night-eighties.css │ │ │ │ ├── tomorrow-night.css │ │ │ │ ├── tomorrow.css │ │ │ │ ├── vs.css │ │ │ │ ├── xcode.css │ │ │ │ ├── xt256.css │ │ │ │ └── zenburn.css │ │ │ ├── marked.js │ │ │ ├── mermaid/ │ │ │ │ └── mermaid.js │ │ │ ├── mindmap/ │ │ │ │ ├── d3@5.js │ │ │ │ ├── transform.js │ │ │ │ └── view.js │ │ │ └── sequence/ │ │ │ ├── sequence-diagram-min.css │ │ │ ├── sequence-diagram-min.js │ │ │ ├── sequence-diagram-raphael-min.js │ │ │ ├── sequence-diagram-raphael.js │ │ │ ├── sequence-diagram-snap-min.js │ │ │ ├── snap.svg-min.js │ │ │ ├── underscore-min.js │ │ │ └── webfont.js │ │ └── plugins/ │ │ ├── code-block-dialog/ │ │ │ └── code-block-dialog.js │ │ ├── emoji-dialog/ │ │ │ ├── emoji-dialog.js │ │ │ └── emoji.json │ │ ├── file-dialog/ │ │ │ └── file-dialog.js │ │ ├── goto-line-dialog/ │ │ │ └── goto-line-dialog.js │ │ ├── help-dialog/ │ │ │ ├── help-dialog.js │ │ │ └── help.md │ │ ├── html-entities-dialog/ │ │ │ ├── html-entities-dialog.js │ │ │ └── html-entities.json │ │ ├── image-dialog/ │ │ │ └── image-dialog.js │ │ ├── link-dialog/ │ │ │ └── link-dialog.js │ │ ├── plugin-template.js │ │ ├── preformatted-text-dialog/ │ │ │ └── preformatted-text-dialog.js │ │ ├── reference-link-dialog/ │ │ │ └── reference-link-dialog.js │ │ ├── table-dialog/ │ │ │ └── table-dialog.js │ │ └── test-plugin/ │ │ └── test-plugin.js │ ├── font-awesome/ │ │ ├── css/ │ │ │ └── font-awesome.css │ │ └── fonts/ │ │ └── FontAwesome.otf │ ├── fonts/ │ │ ├── lato-100.css │ │ └── notosans.css │ ├── jquery/ │ │ └── 1.12.4/ │ │ └── jquery.js │ ├── js/ │ │ ├── array.js │ │ ├── blog.js │ │ ├── cherry_markdown.js │ │ ├── class2browser.js │ │ ├── dingtalk-ddlogin.js │ │ ├── dingtalk-jsapi.js │ │ ├── editor.js │ │ ├── froala-editor.js │ │ ├── html-editor.js │ │ ├── html-to-markdown.js │ │ ├── jquery.form.js │ │ ├── jquery.highlight.js │ │ ├── kancloud.js │ │ ├── main.js │ │ ├── markdown.js │ │ ├── quill.js │ │ ├── sort.js │ │ ├── splitbar.js │ │ ├── wangEditor-plugins/ │ │ │ ├── attach-menu.js │ │ │ ├── history-menu.js │ │ │ ├── release-menu.js │ │ │ └── save-menu.js │ │ ├── word-to-html.js │ │ └── x-frame-bypass-1.0.2.js │ ├── jstree/ │ │ └── 3.3.4/ │ │ ├── jstree.js │ │ └── themes/ │ │ ├── default/ │ │ │ └── style.css │ │ └── default-dark/ │ │ └── style.css │ ├── katex/ │ │ ├── README.md │ │ ├── katex.css │ │ └── katex.js │ ├── layer/ │ │ ├── layer.js │ │ ├── mobile/ │ │ │ ├── layer.js │ │ │ └── need/ │ │ │ └── layer.css │ │ └── skin/ │ │ └── default/ │ │ └── layer.css │ ├── mammoth/ │ │ └── mammoth.browser.js │ ├── mergely/ │ │ ├── editor/ │ │ │ ├── editor.css │ │ │ ├── editor.js │ │ │ ├── editor.php │ │ │ └── lib/ │ │ │ ├── farbtastic/ │ │ │ │ ├── LICENSE.txt │ │ │ │ ├── farbtastic.css │ │ │ │ ├── farbtastic.js │ │ │ │ └── index.html │ │ │ ├── gatag.js │ │ │ ├── tipsy/ │ │ │ │ ├── jquery.tipsy.js │ │ │ │ └── tipsy.css │ │ │ ├── wicked-ui.css │ │ │ └── wicked-ui.js │ │ └── lib/ │ │ ├── codemirror.css │ │ ├── codemirror.js │ │ ├── mergely.css │ │ ├── mergely.js │ │ └── searchcursor.js │ ├── nprogress/ │ │ ├── nprogress.css │ │ └── nprogress.js │ ├── prettify/ │ │ └── themes/ │ │ └── prettify.css │ ├── prismjs/ │ │ ├── prismjs.css │ │ └── prismjs.js │ ├── quill/ │ │ ├── quill.bubble.css │ │ ├── quill.core.css │ │ ├── quill.core.js │ │ ├── quill.icons.js │ │ ├── quill.js │ │ └── quill.snow.css │ ├── table-editor/ │ │ ├── .gitignore │ │ ├── dist/ │ │ │ └── index.js │ │ ├── package.json │ │ ├── src/ │ │ │ └── main.js │ │ └── webpack.config.js │ ├── to-markdown/ │ │ ├── dist/ │ │ │ └── to-markdown.js │ │ └── lib/ │ │ ├── gfm-converters.js │ │ ├── html-parser.js │ │ └── md-converters.js │ ├── turndown/ │ │ └── turndown.js │ ├── vuejs/ │ │ ├── vue.common.js │ │ ├── vue.esm.js │ │ ├── vue.js │ │ ├── vue.runtime.common.js │ │ ├── vue.runtime.esm.js │ │ └── vue.runtime.js │ ├── wangEditor/ │ │ ├── wangEditor.d.ts │ │ └── wangEditor.js │ └── webuploader/ │ ├── README.md │ ├── Uploader.swf │ ├── webuploader.css │ ├── webuploader.custom.js │ ├── webuploader.fis.js │ ├── webuploader.flashonly.js │ ├── webuploader.html5nodepend.js │ ├── webuploader.html5only.js │ ├── webuploader.js │ ├── webuploader.noimage.js │ ├── webuploader.nolog.js │ └── webuploader.withoutimage.js ├── uploads/ │ └── .gitkeep ├── utils/ │ ├── auth2/ │ │ ├── auth2.go │ │ ├── dingtalk/ │ │ │ └── dingtalk.go │ │ └── wecom/ │ │ └── wecom.go │ ├── cryptil/ │ │ └── cryptil.go │ ├── dingtalk/ │ │ └── dingtalk.go │ ├── docx2md.go │ ├── filetil/ │ │ └── filetil.go │ ├── gob.go │ ├── gopool/ │ │ └── gopool.go │ ├── html.go │ ├── krand.go │ ├── ldap.go │ ├── pagination/ │ │ └── pagination.go │ ├── password.go │ ├── requests/ │ │ └── requests.go │ ├── segmenter/ │ │ └── segmenter.go │ ├── sqltil/ │ │ └── sql.go │ ├── template_fun.go │ ├── url.go │ ├── wkhtmltopdf/ │ │ ├── options.go │ │ └── wkhtmltopdf.go │ ├── workweixin/ │ │ └── workweixin.go │ └── ziptil/ │ └── ziptil.go └── views/ ├── account/ │ ├── auth2_callback.tpl │ ├── find_password_setp1.tpl │ ├── find_password_setp2.tpl │ ├── login.tpl │ ├── mail_template.tpl │ └── register.tpl ├── blog/ │ ├── index.tpl │ ├── index_password.tpl │ ├── list.tpl │ ├── manage_edit.tpl │ ├── manage_list.tpl │ └── manage_setting.tpl ├── book/ │ ├── dashboard.tpl │ ├── index.tpl │ ├── setting.tpl │ ├── team.tpl │ └── users.tpl ├── comment/ │ └── index.tpl ├── document/ │ ├── cherry_markdown_edit_template.tpl │ ├── cherry_read.tpl │ ├── compare.tpl │ ├── default_read.tpl │ ├── document_password.tpl │ ├── export.tpl │ ├── froala_edit_template.tpl │ ├── history.tpl │ ├── html_edit_template.tpl │ ├── index.tpl │ ├── kancloud_read_template.tpl │ ├── markdown_edit_template.tpl │ ├── new_html_edit_template.tpl │ ├── template_api-en.tpl │ ├── template_api.tpl │ ├── template_code-en.tpl │ ├── template_code.tpl │ ├── template_normal-en.tpl │ └── template_normal.tpl ├── errors/ │ ├── 403.tpl │ ├── 404.tpl │ └── error.tpl ├── home/ │ └── index.tpl ├── items/ │ ├── index.tpl │ └── list.tpl ├── label/ │ ├── index.tpl │ └── list.tpl ├── manager/ │ ├── attach_detailed.tpl │ ├── attach_list.tpl │ ├── books.tpl │ ├── comments.tpl │ ├── config.tpl │ ├── edit_book.tpl │ ├── edit_users.tpl │ ├── index.tpl │ ├── itemsets.tpl │ ├── label_list.tpl │ ├── setting.tpl │ ├── team.tpl │ ├── team_book_list.tpl │ ├── team_member_list.tpl │ ├── users.tpl │ └── widgets.tpl ├── search/ │ └── index.tpl ├── setting/ │ ├── index.tpl │ └── password.tpl ├── template/ │ └── list.tpl ├── template.tpl └── widgets/ ├── footer.tpl ├── header.tpl └── ie.tpl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.js linguist-language=Go *.css linguist-language=Go ================================================ FILE: .github/ISSUE_TEMPLATE ================================================ 请按照一下格式提交issue,谢谢! 1. 你当前使用的是哪个版本的 MinDoc(`godoc_linux_amd64 version`)? 2. 你当前使用的是什么操作系统? 3. 你是如何操作的? 4. 你期望得到什么结果? 5. 当前遇到的是什么结果? ================================================ FILE: .github/workflows/build.yml ================================================ name: Go on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} outputs: tag: ${{ steps.git.outputs.tag }} strategy: fail-fast: false matrix: config: - { name: "Windows Latest MSVC", artifact: "windows", os: windows-latest } - { name: "Ubuntu Latest GCC", artifact: "linux", os: ubuntu-latest } steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.24.0 - name: Build run: | go mod tidy go build -v # - name: Test # run: go test -v ./... - name: Upload Artifact uses: actions/upload-artifact@v4 with: path: | conf/**/* static/**/* views/**/* mindoc.* name: mindoc-${{ matrix.config.artifact }}-${{ steps.git.outputs.tag }}.7z ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so .DS_Store # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.exe~ mindoc mindoc_linux_amd64 mindoc_linux_musl_amd64 database/mindoc.db *.test *.prof .idea .vscode /conf/app.conf /vendor /runtime /uploads/*.* !/uploads/.gitkeep calibre-*.txz ================================================ FILE: .travis.yml ================================================ os: linux dist: focal language: go go: - "1.18.1" arch: - amd64 env: - GO111MODULE=on CGO_ENABLED=1 install: - go mod tidy -v before_install: - whereis gcc - go env script: - go build -o mindoc_linux_amd64 -ldflags "-w" - cp conf/app.conf.example conf/app.conf - ./mindoc_linux_amd64 version - rm conf/app.conf before_deploy: - go mod tidy -v && GOARCH=amd64 GOOS=linux go build -v -o mindoc_linux_amd64 -ldflags="-w -X 'github.com/mindoc-org/mindoc/conf.VERSION=$TRAVIS_TAG' -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=`date`' -X 'conf.GO_VERSION=`go version`'" # remove files - rm appveyor.yml docker-compose.yml Dockerfile .travis.yml .gitattributes .gitignore go.mod go.sum main.go README.md simsun.ttc start.sh sync_host.sh build_amd64.sh build_musl_amd64.sh # remove dirs - rm -rf cache commands controllers converter .git .github graphics mail models routers utils runtime - ls -alh - cp conf/app.conf.example conf/app.conf - zip -r mindoc_linux_amd64.zip conf static uploads views lib mindoc_linux_amd64 LICENSE.md deploy: provider: releases token: $CI_USER_TOKEN cleanup: true overwrite: true file: - mindoc_linux_amd64.zip on: tags: true branch: master ================================================ FILE: Dockerfile ================================================ FROM golang:bookworm AS build ARG TAG=0.0.1 # 编译-环境变量 ENV GO111MODULE=on ENV GOPROXY=https://goproxy.cn,direct ENV CGO_ENABLED=1 ENV GOARCH=amd64 ENV GOOS=linux # 工作目录 ADD . /go/src/github.com/mindoc-org/mindoc WORKDIR /go/src/github.com/mindoc-org/mindoc # 编译 RUN go env RUN go mod tidy -v RUN go build -v -o mindoc_linux_amd64 -ldflags "-w -s -X 'main.VERSION=$TAG' -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=`go version`'" RUN cp conf/app.conf.example conf/app.conf # 清理不需要的文件 RUN rm appveyor.yml docker-compose.yml Dockerfile .travis.yml .gitattributes .gitignore go.mod go.sum main.go README.md simsun.ttc start.sh conf/*.go RUN rm -rf cache commands controllers converter .git .github graphics mail models routers utils # 测试编译的mindoc是否ok RUN ./mindoc_linux_amd64 version # 必要的文件复制 ADD simsun.ttc /usr/share/fonts/win/ ADD start.sh /go/src/github.com/mindoc-org/mindoc # upgrade to the latest FROM ubuntu:latest # 切换默认shell为bash SHELL ["/bin/bash", "-c"] WORKDIR /mindoc # 文件复制 COPY --from=build /usr/share/fonts/win/simsun.ttc /usr/share/fonts/win/ COPY --from=build /go/src/github.com/mindoc-org/mindoc/mindoc_linux_amd64 /mindoc/ COPY --from=build /go/src/github.com/mindoc-org/mindoc/start.sh /mindoc/ COPY --from=build /go/src/github.com/mindoc-org/mindoc/LICENSE.md /mindoc/ # 文件夹复制 COPY --from=build /go/src/github.com/mindoc-org/mindoc/lib /mindoc/lib COPY --from=build /go/src/github.com/mindoc-org/mindoc/conf /mindoc/__default_assets__/conf COPY --from=build /go/src/github.com/mindoc-org/mindoc/static /mindoc/__default_assets__/static COPY --from=build /go/src/github.com/mindoc-org/mindoc/views /mindoc/__default_assets__/views COPY --from=build /go/src/github.com/mindoc-org/mindoc/uploads /mindoc/__default_assets__/uploads RUN chmod a+r /usr/share/fonts/win/simsun.ttc RUN sed -i "s/archive.ubuntu.com/mirrors.aliyun.com/g" /etc/apt/sources.list /etc/apt/sources.list.d/* # 更新软件包信息 RUN apt-get update # 时区设置(如果不设置, calibre依赖的tzdata在安装过程中会要求选择时区) ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # tzdata的前端类型默认为readline(Shell情况下)或dialog(支持GUI的情况下) ARG DEBIAN_FRONTEND=noninteractive # 安装时区信息 RUN apt install -y --no-install-recommends tzdata # 重新配置tzdata软件包,使得时区设置生效 RUN dpkg-reconfigure --frontend noninteractive tzdata # 安装文泉驿字体 # 安装中文语言包 RUN apt install -y fonts-wqy-microhei fonts-wqy-zenhei locales language-pack-zh-hans-base # 设置默认编码 RUN locale-gen "zh_CN.UTF-8" RUN update-locale LANG=zh_CN.UTF-8 ENV LANG=zh_CN.UTF-8 ENV LANGUAGE=zh_CN:en ENV LC_ALL=zh_CN.UTF-8 # 安装必要依赖、下载、解压 calibre 并清理缓存 RUN apt-get install -y --no-install-recommends \ libglx0 libegl1 libnss3 libxcomposite1 libxkbcommon0 libxdamage1 libxrandr-dev libopengl0 libxtst6 libasound2t64 libxkbfile1\ wget xz-utils && \ mkdir -p /tmp/calibre-cache /opt/calibre && \ wget -O /tmp/calibre-cache/calibre-x86_64.txz -c https://download.calibre-ebook.com/7.26.0/calibre-7.26.0-x86_64.txz --no-check-certificate && \ tar xJof /tmp/calibre-cache/calibre-x86_64.txz -C /opt/calibre && \ rm -rf /tmp/calibre-cache && \ apt-get clean && rm -rf /var/lib/apt/lists/* # 设置环境变量 ENV PATH="/opt/calibre:$PATH" \ QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox" \ QT_QPA_PLATFORM="offscreen" # 测试 calibre 是否可正常使用 RUN ebook-convert --version # refer: https://docs.docker.com/engine/reference/builder/#volume VOLUME ["/mindoc/conf","/mindoc/static","/mindoc/views","/mindoc/uploads","/mindoc/runtime","/mindoc/database"] # refer: https://docs.docker.com/engine/reference/builder/#expose EXPOSE 8181/tcp ENV ZONEINFO=/mindoc/lib/time/zoneinfo.zip RUN chmod +x /mindoc/start.sh ENTRYPOINT ["/bin/bash", "/mindoc/start.sh"] # https://docs.docker.com/engine/reference/commandline/build/#options # docker build --progress plain --rm --build-arg TAG=2.1 --tag gsw945/mindoc:2.1 . # https://docs.docker.com/engine/reference/commandline/run/#options # set MINDOC=//d/mindoc # windows # export MINDOC=/home/ubuntu/mindoc-docker # linux # docker run -d --name=mindoc --restart=always -v /www/mindoc/uploads:/mindoc/uploads -v /www/mindoc/database:/mindoc/database -v /www/mindoc/conf:/mindoc/conf -e MINDOC_DB_ADAPTER=sqlite3 -e MINDOC_DB_DATABASE=./database/mindoc.db -e MINDOC_CACHE=true -e MINDOC_CACHE_PROVIDER=file -p 8181:8181 mindoc-org/mindoc:v2.1 ================================================ FILE: LICENSE.md ================================================ 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. ================================================ FILE: README.md ================================================ # MinDoc 简介 [![Build Status](https://travis-ci.com/mindoc-org/mindoc.svg?branch=master)](https://travis-ci.com/mindoc-org/mindoc) [![Build status](https://ci.appveyor.com/api/projects/status/7680ia6mu29m12wx?svg=true)](https://ci.appveyor.com/project/mindoc-org/mindoc) MinDoc 是一款针对IT团队开发的简单好用的文档管理系统。 MinDoc 的前身是 [SmartWiki](https://github.com/lifei6671/SmartWiki) 文档系统。SmartWiki 是基于 PHP 框架 laravel 开发的一款文档管理系统。因 PHP 的部署对普通用户来说太复杂,所以改用 Golang 开发。可以方便用户部署和实用。 开发缘起是公司IT部门需要一款简单实用的项目接口文档管理和分享的系统。其功能和界面源于 kancloud 。 可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,能够满足大部分中小团队的文档管理需求。 ##### 演示站点&文档: - https://demo.mindoc.cn/docs/mindochelp - https://www.iminho.me/wiki/docs/mindoc/ - https://doc.gsw945.com/docs/mindoc-docs/ --- ### 开发&维护&使用 悉知 - 感谢作者 [lifei6671](https://github.com/lifei6671) 创造了MinDoc,并持续维护了很久。 - 作者因工作等原因,精力有限,无法花费足够的时间来持续维护mindoc,已于北京时间2021年3月23日将mindoc交给社区(github组织[mindoc-org](https://github.com/mindoc-org))维护,期待热心开发者加入[mindoc-org](https://github.com/mindoc-org)一起来维护MinDoc。 - 遇到问题请提 [Issues](https://github.com/mindoc-org/mindoc/issues ),欢迎使用者和贡献者加入QQ群 `1051164153` MinDoc使用&开发交流群 - 对开发感兴趣请关注 [Development](https://github.com/mindoc-org/mindoc/projects/1): - [Todo List](https://github.com/mindoc-org/mindoc/projects/1#column-13554511) - [Work in progress](https://github.com/mindoc-org/mindoc/projects/1#column-13554512) - [Review in progress](https://github.com/mindoc-org/mindoc/projects/1#column-13554513) - Mindoc基于 [beeego](https://github.com/beego/beego) 开发,beego文档地址: https://github.com/beego/beego-doc/tree/main/docs/zh - :warning: **特别声明**: - 原作者 [lifei6671](https://github.com/lifei6671) 已于 2021-08-06 删除了个人捐赠信息,参见: [1a179179c1fe4d0d4db95e0b757d863aee5bf395](https://github.com/mindoc-org/mindoc/commit/1a179179c1fe4d0d4db95e0b757d863aee5bf395) - 截止目前(2023-03-27),[mindoc-org](https://github.com/mindoc-org) 暂未发布任何捐赠信息,请勿轻信 --- # 安装与使用 ~~如果你的服务器上没有安装golang程序请手动设置一个环境变量如下:键名为 ZONEINFO,值为MinDoc跟目录下的/lib/time/zoneinfo.zip 。~~ 更多信息请查看手册: [MinDoc 使用手册](https://demo.mindoc.cn/docs/mindochelp/mindoc-summary) 对于没有Golang使用经验的用户,可以从 [https://github.com/mindoc-org/mindoc/releases](https://github.com/mindoc-org/mindoc/releases) 这里下载编译完的程序。 如果有Golang开发经验,建议通过编译安装,要求golang版本不小于1.23.0(需支持`CGO`、`go mod`和`import _ "time/tzdata"`)(推荐Go版本为1.23.x)。 > 注意: CentOS7上GLibC版本低,常规编译版本不能使用。需要自行源码编译,或使用使用musl编译版本。 ## 常规编译 ```bash # 克隆源码 git clone https://github.com/mindoc-org/mindoc.git # go包安装 go mod tidy -v # 编译(sqlite需要CGO支持) go build -ldflags "-w" -o mindoc main.go # 数据库初始化(此步骤执行之前,需配置`conf/app.conf`) ./mindoc install # 执行 ./mindoc # 开发阶段运行 bee run ``` ## 旧版本运行 可更新部分数据库配置 ```base ./mindoc update ``` MinDoc 如果使用MySQL储存数据,则编码必须是`utf8mb4_general_ci`。请在安装前,把数据库配置填充到项目目录下的 `conf/app.conf` 中。 如果使用 `SQLite` 数据库,则直接在配置文件中配置数据库路径即可. 如果conf目录下不存在 `app.conf` 请重命名 `app.conf.example` 为 `app.conf`。 **默认程序会自动初始化一个超级管理员用户:admin 密码:123456 。请登录后重新设置密码。** ## Linux系统中不依赖gLibC的编译方式 ### 安装 musl-gcc ```bash # 手动安装 wget -c http://musl.libc.org/releases/musl-1.2.2.tar.gz tar -xvf musl-1.2.2.tar.gz cd musl-1.2.2 ./configure make sudo make install # apt 安装 sudo apt install musl-tools ``` ### 使用 musl-gcc 编译 mindoc ```bash go mod tidy -v export GOARCH=amd64 export GOOS=linux # 设置使用musl-gcc export CC=/usr/local/musl/bin/musl-gcc # 设置版本 export TRAVIS_TAG=temp-musl-v`date +%y%m%d` go build -v -o mindoc_linux_musl_amd64 -ldflags="-linkmode external -extldflags '-static' -w -X 'github.com/mindoc-org/mindoc/conf.VERSION=$TRAVIS_TAG' -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=`date`' -X 'github.com/mindoc-org/mindoc/conf.GO_VERSION=`go version`'" # 验证 ./mindoc_linux_musl_amd64 version ``` ## Windows 上后台运行 使用 [mindoc-daemon](https://github.com/mindoc-org/mindoc-daemon) ```ini #邮件配置-示例 #是否启用邮件 enable_mail=true #smtp服务器的账号 smtp_user_name=admin@iminho.me #smtp服务器的地址 smtp_host=smtp.ym.163.com #密码 smtp_password=1q2w3e__ABC #端口号 smtp_port=25 #邮件发送人的地址 form_user_name=admin@iminho.me #邮件有效期30分钟 mail_expired=30 ``` # 使用Docker部署 如果是Docker用户,可参考项目内置的Dockerfile文件自行编译镜像(编译命令见Dockerfile文件底部注释,仅供参考)。 在启动镜像时需要提供如下的常用环境变量(全部支持的环境变量请参考: [`conf/app.conf.example`](https://github.com/mindoc-org/mindoc/blob/master/conf/app.conf.example)): ```ini DB_ADAPTER 指定DB类型(默认为sqlite) MYSQL_PORT_3306_TCP_ADDR MySQL地址 MYSQL_PORT_3306_TCP_PORT MySQL端口号 MYSQL_INSTANCE_NAME MySQL数据库名称 MYSQL_USERNAME MySQL账号 MYSQL_PASSWORD MySQL密码 HTTP_PORT 程序监听的端口号 MINDOC_ENABLE_EXPORT 开启导出(默认为false) ``` #### 举个栗子-当前(公开)镜像(信息页面: https://cr.console.aliyun.com/images/cn-hangzhou/mindoc-org/mindoc/detail , 需要登录阿里云账号才可访问列表) ##### Windows ```bash set MINDOC=//d/mindoc docker run -it --name=mindoc --restart=always -v "%MINDOC%/conf":"/mindoc/conf" -p 8181:8181 -e MINDOC_ENABLE_EXPORT=true -d registry.cn-hangzhou.aliyuncs.com/mindoc-org/mindoc:v2.2-beta.2 ``` ##### Linux、Mac ```bash export MINDOC=/home/ubuntu/mindoc-docker docker run -it --name=mindoc --restart=always -v "${MINDOC}/conf":"/mindoc/conf" -p 8181:8181 -e MINDOC_ENABLE_EXPORT=true -d registry.cn-hangzhou.aliyuncs.com/mindoc-org/mindoc:v2.2-beta.2 ``` ##### 举个栗子-更多环境变量示例(镜像已过期,仅供参考,请以当前镜像为准) ```bash docker run -p 8181:8181 --name mindoc -e DB_ADAPTER=mysql -e MYSQL_PORT_3306_TCP_ADDR=10.xxx.xxx.xxx -e MYSQL_PORT_3306_TCP_PORT=3306 -e MYSQL_INSTANCE_NAME=mindoc -e MYSQL_USERNAME=root -e MYSQL_PASSWORD=123456 -e httpport=8181 -d daocloud.io/lifei6671/mindoc:latest ``` #### dockerfile内容参考 - [无需代理直接加速各种 GitHub 资源拉取 | 国内镜像赋能 | 助力开发](https://blog.frytea.com/archives/504/) - [阿里云 - Ubuntu 镜像](https://developer.aliyun.com/mirror/ubuntu) ### docker-compose 一键安装 1. 修改配置文件 修改`docker-compose.yml`中的配置信息,主要修改`volumes`节点,将宿主机的两个目录映射到容器内。 `environment`节点,配置自己的环境变量。 2. 一键完成所有环境搭建 > docker-compose up -d 3. 浏览器访问 > http://localhost:8181/ 整个部署完成了 4. 常用命令参考 - 启动 > docker-compose up -d - 停止 > docker-compose stop - 重启 > docker-compose restart - 停止删除容器,释放所有资源 > docker-compose down - 删除并重新创建 > docker-compose -f docker-compose.yml down && docker-compose up -d > > 更多 docker-compose 的使用相关的内容 请查看官网文档或百度 #### MCP服务器对接指导 1. 请在配置文件中启用MCP服务器功能 在配置文件`app.conf`中添加或修改为如下内容: ``` # MCP Server 功能 enable_mcp_server="${MINDOC_ENABLE_MCP_SERVER||true}" mcp_api_key="${MINDOC_MCP_API_KEY||demo-mcp-api-key}" ``` 说明: `enable_mcp_server`为是否启用MCP服务器功能,默认为true。 `mcp_api_key` 为MCP服务器的API密钥,示例配置中默认为`demo-mcp-api-key`,可根据需求自行修改。 2. 在Dify等AI应用或其他可调用MCP服务器的项目配置中添加如下Mindoc配置 ```json { "mindoc": { "transport": "streamable_http", "url": "http://127.0.0.1:8181/mcp/?api_key=demo-mcp-api-key", "headers":{}, "timeout":600 } } ``` 说明: `transport`为传输方式,目前支持`streamable_http`。 `url`为Mindoc的MCP服务地址,示例配置中Endpoint默认为`http://127.0.0.1:8181`,默认的API密钥为`demo-mcp-api-key`,可自行修改为对接时项目实际使用的Endpoint和API密钥。 # 项目截图 **创建项目** ![创建项目](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/create.png?raw=true) **项目列表** ![项目列表](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/project_list.png?raw=true) **项目概述** ![项目概述](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/intro.png?raw=true) **项目成员** ![项目成员](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/member.png?raw=true) **项目设置** ![项目设置](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/project_setting.png?raw=true) **基于Editor.md开发的Markdown编辑器** ![基于Editor.md开发的Markdown编辑器](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/editor_md.png?raw=true) **基于wangEditor开发的富文本编辑器** ![基于wangEditor开发的富文本编辑器](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/wang_editor.png?raw=true) **基于cherryMarkdown开发的编辑器** ![基于cherry-markdown开发的编辑器](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/cheery-markdown.png?raw=true) **项目预览** ![项目预览](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/preview.png?raw=true) **超级管理员后台** ![超级管理员后台](https://github.com/mindoc-org/mindoc/blob/master/uploads/docs/admin.png?raw=true) # 使用的技术(TODO: 最新技术栈整理中,使用的第三方库升级中) - [Beego](https://github.com/beego/beego) ~~1.10.0~~ - MySQL 5.6 - [editor.md](https://github.com/pandao/editor.md) Markdown 编辑器 - [cherry-markdown](https://github.com/Tencent/cherry-markdown) Cherry Markdown Writer - [Bootstrap](https://github.com/twbs/bootstrap) 3.2 - [jQuery](https://github.com/jquery/jquery) 库 - [WebUploader](https://github.com/fex-team/webuploader) 文件上传框架 - [NProgress](https://github.com/rstacruz/nprogress) 库 - [jsTree](https://github.com/vakata/jstree) 树状结构库 - [Font Awesome](https://github.com/FortAwesome/Font-Awesome) 字体库 - [Cropper](https://github.com/fengyuanchen/cropper) 图片剪裁库 - [layer](https://github.com/sentsin/layer) 弹出层框架 - [highlight.js](https://github.com/highlightjs/highlight.js) 代码高亮库 - ~~to-markdown~~[Turndown](https://github.com/domchristie/turndown) HTML转Markdown库 - ~~quill 富文本编辑器~~ - [wangEditor](https://github.com/wangeditor-team/wangEditor) 富文本编辑器 - 参考 - [wangEditor v4.7 富文本编辑器教程](https://www.bookstack.cn/books/wangeditor-4.7-zh) - [扩展菜单注册太过繁琐 #2493](https://github.com/wangeditor-team/wangEditor/issues/2493) - 工具: `https://babeljs.io/repl` + `@babel/plugin-transform-classes` - [Vue.js](https://github.com/vuejs/vue) 框架 - [MCP-Go](https://github.com/mark3labs/mcp-go) # 主要功能 - 项目管理,可以对项目进行编辑更改,成员添加, 项目排序等。 - 文档管理,添加和删除文档等。 - 评论管理,可以管理文档评论和自己发布的评论。 - 用户管理,添加和禁用用户,个人资料更改等。 - 用户权限管理 , 实现用户角色的变更。 - 项目加密,可以设置项目公开状态,私有项目需要通过Token访问。 - 站点配置,多语言切换, 可开启匿名访问、验证码等。 # 参与开发 我们欢迎您在 MinDoc 项目的 GitHub 上报告 issue 或者 pull request。 如果您还不熟悉GitHub的Fork and Pull开发模式,您可以阅读GitHub的文档(https://help.github.com/articles/using-pull-requests) 获得更多的信息。 # 关于作者[lifei6671](https://github.com/lifei6671) 一个不纯粹的PHPer,一个不自由的 gopher 。 # 部署补充 - 若内网部署,draw.io无法使用外网,则需要用tomcat运行war包,见(https://github.com/jgraph/drawio) 从release下载,之后修改markdown.js的TODO行对应的链接即可 - 为了护眼,简单增加了编辑界面的主题切换,见editormd.js和markdown_edit_template.tpl - (需重新编译项)为了对已删除文档/文档引用图片删除文字后,对悬空无引用的图片/附件进行清理,增加了清理接口,需重新编译 - 编译后除二进制文件外还需更新三个文件: conf/lang/en-us.ini,zh-cn.ini; attach_list.tpl - 若不想重新编译,也可通过database/clean.py,手动执行对无引用图片/附件的文件清理和数据库记录双向清理。 - 若采用nginx二级部署,以yourpath/为例,需修改 - conf/app.conf修改:`baseurl="/yourpath"` - static/js/kancloud.js文件中`url: "/comment/xxxxx` => `url: "/yourpath" + "/comment/xxxxx`, 共两处 - nginx端口代理示例: ``` 增加 location /yourpath/ { rewrite ^/yourpath/(.*) /$1 break; proxy_pass http://127.0.0.1:8181; } ``` 注意使用的是127.0.0.1,根据自身选择替换,如果nginx是docker部署,则还需要在docker中托管运行mindoc,具体参考如下配置: - docker-compose代理示例(docker-nginx代理运行mindoc) ``` version: '3' services: mynginx: image: nginx:latest ports: - "8880:80" command: - bash - -c - | service nginx start cd /src/mindoc/ && ./mindoc volumes: - ..:/src - ./nginx:/etc/nginx/conf.d ``` 目录结构 ``` onefolder | - docker | - docker-compose.yml - nginx | - mynginx.conf - mindoc | - database/ - conf/ - ... ``` ================================================ FILE: appveyor.yml ================================================ version: 1.0.{build} branches: only: - master image: Visual Studio 2022 clone_folder: c:\gopath\src\github.com\mindoc-org\mindoc init: - cmd: >- if [%tbs_arch%]==[x86] SET PATH=C:\msys64\mingw32\bin;%PATH% if [%tbs_arch%]==[x64] SET PATH=C:\msys64\mingw64\bin;%PATH% SET PATH=%GOPATH%\bin;%GOBIN%;%PATH% FOR /f "delims=" %%i IN ('go version') DO (SET GO_VERSION=%%i) git config --global --add safe.directory /cygdrive/c/gopath/src/github.com/mindoc-org/mindoc environment: GOPATH: c:\gopath GOBIN: c:\gobin GO111MODULE: on CGO_ENABLED: 1 matrix: - tbs_arch: x86 GOARCH: 386 job_name: job_x86 - tbs_arch: x64 GOARCH: amd64 job_name: job_x64 install: - cmd: >- echo %PATH% echo %GO_VERSION% go env where gcc where g++ build_script: - cmd: >- cd c:\gopath\src\github.com\mindoc-org\mindoc go mod tidy -v go build -v -o "mindoc_windows_%GOARCH%.exe" -ldflags="-w -X github.com/mindoc-org/mindoc/conf.VERSION=%APPVEYOR_REPO_TAG_NAME% -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=%date% %time%' -X 'github.com/mindoc-org/mindoc/conf.GO_VERSION=%GO_VERSION%'" 7z a -t7z -r mindoc_windows_%GOARCH%.7z conf/*.conf* conf/lang/* static/* mindoc_windows_%GOARCH%.exe views/* uploads/* lib/* LICENSE.md test_script: - cmd: >- cd c:\gopath\src\github.com\mindoc-org\mindoc pwsh -NoProfile -ExecutionPolicy Bypass -Command "& {Copy-Item -Force -Path 'conf\app.conf.example' -Destination 'conf\app.conf'}" mindoc_windows_%GOARCH%.exe version artifacts: - path: mindoc_windows_*.7z deploy: off ================================================ FILE: build_amd64.sh ================================================ rm mindoc_linux_amd64 mindoc_linux_musl_amd64 rm -rf ../mindoc_linux_amd64/ export GOARCH=amd64 export GOOS=linux export CC=/usr/bin/gcc export TRAVIS_TAG=v2.1-beta.6 go mod tidy -v go build -v -o mindoc_linux_amd64 -ldflags="-linkmode external -extldflags '-static' -w -X 'github.com/mindoc-org/mindoc/conf.VERSION=$TRAVIS_TAG' -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=`date`' -X 'github.com/mindoc-org/mindoc/conf.GO_VERSION=`go version`'" ./mindoc_linux_amd64 version mkdir ../mindoc_linux_amd64 cp -r * ../mindoc_linux_amd64 cd ../mindoc_linux_amd64 rm -rf cache commands controllers converter .git .github graphics mail models routers utils runtime conf/*.go rm appveyor.yml docker-compose.yml Dockerfile .travis.yml .gitattributes .gitignore go.mod go.sum main.go README.md simsun.ttc start.sh sync_host.sh build_amd64.sh build_musl_amd64.sh zip -r mindoc_linux_amd64.zip conf static uploads views lib mindoc_linux_amd64 LICENSE.md mv ./mindoc_linux_amd64.zip ../ ================================================ FILE: build_musl_amd64.sh ================================================ rm mindoc_linux_musl_amd64 mindoc_linux_amd64 rm -rf ../mindoc_linux_musl_amd64/ export GOARCH=amd64 export GOOS=linux export CC=/usr/local/musl/bin/musl-gcc export TRAVIS_TAG=v2.1-beta.6 go mod tidy -v go build -v -o mindoc_linux_musl_amd64 -ldflags="-linkmode external -extldflags '-static' -w -X 'github.com/mindoc-org/mindoc/conf.VERSION=$TRAVIS_TAG' -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=`date`' -X 'github.com/mindoc-org/mindoc/conf.GO_VERSION=`go version`'" ./mindoc_linux_musl_amd64 version mkdir ../mindoc_linux_musl_amd64 cp -r * ../mindoc_linux_musl_amd64 cd ../mindoc_linux_musl_amd64 rm -rf cache commands controllers converter .git .github graphics mail models routers utils runtime conf/*.go rm appveyor.yml docker-compose.yml Dockerfile .travis.yml .gitattributes .gitignore go.mod go.sum main.go README.md simsun.ttc start.sh sync_host.sh build_amd64.sh build_musl_amd64.sh zip -r mindoc_linux_musl_amd64.zip conf static uploads views lib mindoc_linux_musl_amd64 LICENSE.md mv ./mindoc_linux_musl_amd64.zip ../ ================================================ FILE: cache/cache.go ================================================ package cache import ( "bytes" "context" "encoding/gob" "errors" "time" "github.com/beego/beego/v2/client/cache" "github.com/beego/beego/v2/core/logs" ) var bm cache.Cache var nilctx = context.TODO() func Get(key string, e interface{}) error { val, err := bm.Get(nilctx, key) if err != nil { return errors.New("get cache error:" + err.Error()) } if val == nil { return errors.New("cache does not exist") } if b, ok := val.([]byte); ok { buf := bytes.NewBuffer(b) decoder := gob.NewDecoder(buf) err := decoder.Decode(e) if err != nil { logs.Error("反序列化对象失败 ->", err) } return err } else if s, ok := val.(string); ok && s != "" { buf := bytes.NewBufferString(s) decoder := gob.NewDecoder(buf) err := decoder.Decode(e) if err != nil { logs.Error("反序列化对象失败 ->", err) } return err } return errors.New("value is not []byte or string") } func Put(key string, val interface{}, timeout time.Duration) error { var buf bytes.Buffer encoder := gob.NewEncoder(&buf) err := encoder.Encode(val) if err != nil { logs.Error("序列化对象失败 ->", err) return err } return bm.Put(nilctx, key, buf.String(), timeout) } func Delete(key string) error { return bm.Delete(nilctx, key) } func Incr(key string) error { return bm.Incr(nilctx, key) } func Decr(key string) error { return bm.Decr(nilctx, key) } func IsExist(key string) (bool, error) { return bm.IsExist(nilctx, key) } func ClearAll() error { return bm.ClearAll(nilctx) } func StartAndGC(config string) error { return bm.StartAndGC(config) } //Init will initialize cache func Init(c cache.Cache) { bm = c } ================================================ FILE: cache/cache_null.go ================================================ package cache import ( "context" "time" ) type NullCache struct { } func (bm *NullCache) Get(ctx context.Context, key string) (interface{}, error) { return nil, nil } func (bm *NullCache)GetMulti(ctx context.Context, keys []string) ([]interface{}, error) { return nil, nil } func (bm *NullCache)Put(ctx context.Context,key string, val interface{}, timeout time.Duration) error { return nil } func (bm *NullCache)Delete(ctx context.Context,key string) error { return nil } func (bm *NullCache)Incr(ctx context.Context,key string) error { return nil } func (bm *NullCache)Decr(ctx context.Context,key string) error { return nil } func (bm *NullCache)IsExist(ctx context.Context,key string) (bool, error) { return false, nil } func (bm *NullCache)ClearAll(ctx context.Context) error{ return nil } func (bm *NullCache)StartAndGC(config string) error { return nil } ================================================ FILE: commands/command.go ================================================ package commands import ( "encoding/gob" "flag" "fmt" "log" "net/url" "os" "path/filepath" "strconv" "strings" "time" _ "time/tzdata" "bytes" "encoding/json" "net/http" beegoCache "github.com/beego/beego/v2/client/cache" _ "github.com/beego/beego/v2/client/cache/memcache" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/howeyc/fsnotify" _ "github.com/lib/pq" "github.com/lifei6671/gocaptcha" "github.com/mindoc-org/mindoc/cache" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils/filetil" ) // RegisterDataBase 注册数据库 func RegisterDataBase() { logs.Info("正在初始化数据库配置.") dbadapter, _ := web.AppConfig.String("db_adapter") orm.DefaultTimeLoc = time.Local orm.DefaultRowsLimit = -1 if strings.EqualFold(dbadapter, "mysql") { host, _ := web.AppConfig.String("db_host") database, _ := web.AppConfig.String("db_database") username, _ := web.AppConfig.String("db_username") password, _ := web.AppConfig.String("db_password") timezone, _ := web.AppConfig.String("timezone") location, err := time.LoadLocation(timezone) if err == nil { orm.DefaultTimeLoc = location } else { logs.Error("加载时区配置信息失败,请检查是否存在 ZONEINFO 环境变量->", err) } port, _ := web.AppConfig.String("db_port") dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s", username, password, host, port, database, url.QueryEscape(timezone)) if err := orm.RegisterDataBase("default", "mysql", dataSource); err != nil { logs.Error("注册默认数据库失败->", err) os.Exit(1) } } else if strings.EqualFold(dbadapter, "sqlite3") { database, _ := web.AppConfig.String("db_database") if strings.HasPrefix(database, "./") { database = filepath.Join(conf.WorkingDirectory, string(database[1:])) } if p, err := filepath.Abs(database); err == nil { database = p } dbPath := filepath.Dir(database) if _, err := os.Stat(dbPath); err != nil && os.IsNotExist(err) { _ = os.MkdirAll(dbPath, 0777) } err := orm.RegisterDataBase("default", "sqlite3", database) if err != nil { logs.Error("注册默认数据库失败->", err) } } else if strings.EqualFold(dbadapter, "postgres") { host, _ := web.AppConfig.String("db_host") database, _ := web.AppConfig.String("db_database") username, _ := web.AppConfig.String("db_username") password, _ := web.AppConfig.String("db_password") sslmode, _ := web.AppConfig.String("db_sslmode") timezone, _ := web.AppConfig.String("timezone") location, err := time.LoadLocation(timezone) if err == nil { orm.DefaultTimeLoc = location } else { logs.Error("加载时区配置信息失败,请检查是否存在 ZONEINFO 环境变量->", err) } port, _ := web.AppConfig.String("db_port") dataSource := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", username, password, host, port, database, sslmode) if err := orm.RegisterDataBase("default", "postgres", dataSource); err != nil { logs.Error("注册默认数据库失败->", err) os.Exit(1) } } else { logs.Error("不支持的数据库类型.") os.Exit(1) } logs.Info("数据库初始化完成.") } // RegisterModel 注册Model func RegisterModel() { orm.RegisterModelWithPrefix(conf.GetDatabasePrefix(), new(models.Member), new(models.Book), new(models.Relationship), new(models.Option), new(models.Document), new(models.Attachment), new(models.Logger), new(models.MemberToken), new(models.DocumentHistory), new(models.Migration), new(models.Label), new(models.Blog), new(models.Template), new(models.Team), new(models.TeamMember), new(models.TeamRelationship), new(models.Itemsets), new(models.Comment), new(models.WorkWeixinAccount), new(models.DingTalkAccount), new(models.ContentReverseIndex), ) gob.Register(models.Blog{}) gob.Register(models.Document{}) gob.Register(models.Template{}) //migrate.RegisterMigration() err := orm.RunSyncdb("default", false, true) if err != nil { logs.Error("注册Model失败 ->", err) os.Exit(1) } } // RegisterLogger 注册日志 func RegisterLogger(log string) { logs.Reset() logs.SetLogFuncCall(true) _ = logs.SetLogger("console") logs.EnableFuncCallDepth(true) if web.AppConfig.DefaultBool("log_is_async", true) { logs.Async(1e3) } if log == "" { logPath, err := filepath.Abs(web.AppConfig.DefaultString("log_path", conf.WorkingDir("runtime", "logs"))) if err == nil { log = logPath } else { log = conf.WorkingDir("runtime", "logs") } } logPath := filepath.Join(log, "log.log") if _, err := os.Stat(log); os.IsNotExist(err) { _ = os.MkdirAll(log, 0755) } config := make(map[string]interface{}, 1) config["filename"] = logPath config["perm"] = "0755" config["rotate"] = true if maxLines := web.AppConfig.DefaultInt("log_maxlines", 1000000); maxLines > 0 { config["maxLines"] = maxLines } if maxSize := web.AppConfig.DefaultInt("log_maxsize", 1<<28); maxSize > 0 { config["maxsize"] = maxSize } if !web.AppConfig.DefaultBool("log_daily", true) { config["daily"] = false } if maxDays := web.AppConfig.DefaultInt("log_maxdays", 7); maxDays > 0 { config["maxdays"] = maxDays } if level := web.AppConfig.DefaultString("log_level", "Trace"); level != "" { switch level { case "Emergency": config["level"] = logs.LevelEmergency case "Alert": config["level"] = logs.LevelAlert case "Critical": config["level"] = logs.LevelCritical case "Error": config["level"] = logs.LevelError case "Warning": config["level"] = logs.LevelWarning case "Notice": config["level"] = logs.LevelNotice case "Informational": config["level"] = logs.LevelInformational case "Debug": config["level"] = logs.LevelDebug } } b, err := json.Marshal(config) if err != nil { logs.Error("初始化文件日志时出错 ->", err) _ = logs.SetLogger("file", `{"filename":"`+logPath+`"}`) } else { _ = logs.SetLogger(logs.AdapterFile, string(b)) } logs.SetLogFuncCall(true) } // RunCommand 注册orm命令行工具 func RegisterCommand() { if len(os.Args) >= 2 && os.Args[1] == "install" { ResolveCommand(os.Args[2:]) Install() } else if len(os.Args) >= 2 && os.Args[1] == "version" { CheckUpdate() os.Exit(0) } else if len(os.Args) >= 2 && os.Args[1] == "update" { Update() os.Exit(0) } } // 注册模板函数 func RegisterFunction() { err := web.AddFuncMap("config", models.GetOptionValue) if err != nil { logs.Error("注册函数 config 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("cdn", func(p string) string { cdn := web.AppConfig.DefaultString("cdn", "") if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") { return p } //如果没有设置cdn,则使用baseURL拼接 if cdn == "" { baseUrl := web.AppConfig.DefaultString("baseurl", "") if strings.HasPrefix(p, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + p[1:] } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + p } return baseUrl + p } if strings.HasPrefix(p, "/") && strings.HasSuffix(cdn, "/") { return cdn + string(p[1:]) } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(cdn, "/") { return cdn + "/" + p } return cdn + p }) if err != nil { logs.Error("注册函数 cdn 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("cdnjs", conf.URLForWithCdnJs) if err != nil { logs.Error("注册函数 cdnjs 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("cdncss", conf.URLForWithCdnCss) if err != nil { logs.Error("注册函数 cdncss 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("cdnimg", conf.URLForWithCdnImage) if err != nil { logs.Error("注册函数 cdnimg 出错 ->", err) os.Exit(-1) } //重写url生成,支持配置域名以及域名前缀 err = web.AddFuncMap("urlfor", conf.URLFor) if err != nil { logs.Error("注册函数 urlfor 出错 ->", err) os.Exit(-1) } //读取配置值(未作任何转换) err = web.AddFuncMap("conf", conf.CONF) if err != nil { logs.Error("注册函数 conf 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("date_format", func(t time.Time, format string) string { return t.Local().Format(format) }) if err != nil { logs.Error("注册函数 date_format 出错 ->", err) os.Exit(-1) } err = web.AddFuncMap("i18n", i18n.Tr) if err != nil { logs.Error("注册函数 i18n 出错 ->", err) os.Exit(-1) } i18nList, err := web.AppConfig.String("i18n_list") if err != nil { logs.Error("error : failed to read i18n_list config ->", err) i18nList = "" } if i18nList == "" { // 之所以分开判断是因为读取出的配置也可能是空串 logs.Error("error : config `i18n_list` is empty, please add config item like format: `i18n_list=zh-CN:简体中文|en-US:English`") i18nList = "zh-cn:简体中文|en-us:English|ru-ru:Русский" // 没有配置时给个默认配置,避免啥语言都没有 } langs := strings.Split(i18nList, "|") i18nMap := make(map[string]string) for _, langItem := range langs { langItemSplit := strings.Split(langItem, ":") if len(langItemSplit) < 2 { logs.Error("error: language config value `" + langItem + "` for `i18n_list` format error") continue } lang := langItemSplit[0] i18nMap[lang] = langItemSplit[1] if err := i18n.SetMessage(lang, "conf/lang/"+lang+".ini"); err != nil { logs.Error("Fail to set message file: " + err.Error()) return } } i18nMapBytes, err := json.Marshal(i18nMap) if err != nil { logs.Error("error: Fail to marshal i18n map, " + err.Error()) i18nMapBytes = []byte("{}") } err = web.AppConfig.Set("i18n_map", string(i18nMapBytes)) if err != nil { logs.Error("error: Fail to set i18n_map, " + err.Error()) } } // 解析命令 func ResolveCommand(args []string) { flagSet := flag.NewFlagSet("MinDoc command: ", flag.ExitOnError) flagSet.StringVar(&conf.ConfigurationFile, "config", "", "MinDoc configuration file.") flagSet.StringVar(&conf.WorkingDirectory, "dir", "", "MinDoc working directory.") flagSet.StringVar(&conf.LogFile, "log", "", "MinDoc log file path.") if err := flagSet.Parse(args); err != nil { log.Fatal("解析命令失败 ->", err) } if conf.WorkingDirectory == "" { if p, err := filepath.Abs(os.Args[0]); err == nil { conf.WorkingDirectory = filepath.Dir(p) } } if conf.ConfigurationFile == "" { conf.ConfigurationFile = conf.WorkingDir("conf", "app.conf") config := conf.WorkingDir("conf", "app.conf.example") if !filetil.FileExists(conf.ConfigurationFile) && filetil.FileExists(config) { _ = filetil.CopyFile(conf.ConfigurationFile, config) } } if err := gocaptcha.ReadFonts(conf.WorkingDir("static", "fonts"), ".ttf"); err != nil { log.Fatal("读取字体文件时出错 -> ", err) } if err := web.LoadAppConfig("ini", conf.ConfigurationFile); err != nil { log.Fatal("An error occurred:", err) } if conf.LogFile == "" { logPath, err := filepath.Abs(web.AppConfig.DefaultString("log_path", conf.WorkingDir("runtime", "logs"))) if err == nil { conf.LogFile = logPath } else { conf.LogFile = conf.WorkingDir("runtime", "logs") } } conf.AutoLoadDelay = web.AppConfig.DefaultInt("config_auto_delay", 0) uploads := conf.WorkingDir("uploads") _ = os.MkdirAll(uploads, 0666) web.BConfig.WebConfig.StaticDir["/static"] = filepath.Join(conf.WorkingDirectory, "static") web.BConfig.WebConfig.StaticDir["/uploads"] = uploads web.BConfig.WebConfig.ViewsPath = conf.WorkingDir("views") web.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteDefaultMode var upload_file_size = conf.GetUploadFileSize() if upload_file_size > web.BConfig.MaxUploadSize { web.BConfig.MaxUploadSize = upload_file_size } fonts := conf.WorkingDir("static", "fonts") if !filetil.FileExists(fonts) { log.Fatal("Font path not exist.") } if err := gocaptcha.ReadFonts(filepath.Join(conf.WorkingDirectory, "static", "fonts"), ".ttf"); err != nil { log.Fatal("读取字体失败 ->", err) } RegisterDataBase() RegisterCache() RegisterModel() RegisterLogger(conf.LogFile) models.InitializeMissingIndexes() ModifyPassword() } // 注册缓存管道 func RegisterCache() { isOpenCache := web.AppConfig.DefaultBool("cache", false) if !isOpenCache { cache.Init(&cache.NullCache{}) return } logs.Info("正常初始化缓存配置.") cacheProvider, _ := web.AppConfig.String("cache_provider") if cacheProvider == "file" { cacheFilePath := web.AppConfig.DefaultString("cache_file_path", "./runtime/cache/") if strings.HasPrefix(cacheFilePath, "./") { cacheFilePath = filepath.Join(conf.WorkingDirectory, string(cacheFilePath[1:])) } fileCache := beegoCache.NewFileCache() fileConfig := make(map[string]string, 0) fileConfig["CachePath"] = cacheFilePath fileConfig["DirectoryLevel"] = web.AppConfig.DefaultString("cache_file_dir_level", "2") fileConfig["EmbedExpiry"] = web.AppConfig.DefaultString("cache_file_expiry", "120") fileConfig["FileSuffix"] = web.AppConfig.DefaultString("cache_file_suffix", ".bin") bc, err := json.Marshal(&fileConfig) if err != nil { logs.Error("初始化file缓存失败:", err) os.Exit(1) } _ = fileCache.StartAndGC(string(bc)) cache.Init(fileCache) } else if cacheProvider == "memory" { cacheInterval := web.AppConfig.DefaultInt("cache_memory_interval", 60) memory := beegoCache.NewMemoryCache() beegoCache.DefaultEvery = cacheInterval cache.Init(memory) } else if cacheProvider == "redis" { var redisConfig struct { Conn string `json:"conn"` Password string `json:"password"` DbNum string `json:"dbNum"` Key string `json:"key"` } //设置Redis前缀 if key := web.AppConfig.DefaultString("cache_redis_prefix", ""); key != "" { redisConfig.Key = key // 设置Redis前缀,替代原来的 redis.DefaultKey } redisConfig.DbNum = "0" redisConfig.Conn = web.AppConfig.DefaultString("cache_redis_host", "") if pwd := web.AppConfig.DefaultString("cache_redis_password", ""); pwd != "" { redisConfig.Password = pwd } if dbNum := web.AppConfig.DefaultInt("cache_redis_db", 0); dbNum > 0 { redisConfig.DbNum = strconv.Itoa(dbNum) } bc, err := json.Marshal(&redisConfig) if err != nil { logs.Error("初始化Redis缓存失败:", err) os.Exit(1) } redisCache, err := beegoCache.NewCache("redis", string(bc)) if err != nil { logs.Error("初始化Redis缓存失败:", err) os.Exit(1) } cache.Init(redisCache) } else if cacheProvider == "memcache" { var memcacheConfig struct { Conn string `json:"conn"` } memcacheConfig.Conn = web.AppConfig.DefaultString("cache_memcache_host", "") bc, err := json.Marshal(&memcacheConfig) if err != nil { logs.Error("初始化 Memcache 缓存失败 ->", err) os.Exit(1) } memcache, err := beegoCache.NewCache("memcache", string(bc)) if err != nil { logs.Error("初始化 Memcache 缓存失败 ->", err) os.Exit(1) } cache.Init(memcache) } else { cache.Init(&cache.NullCache{}) logs.Warn("不支持的缓存管道,缓存将禁用 ->", cacheProvider) return } logs.Info("缓存初始化完成.") } // 自动加载配置文件.修改了监听端口号和数据库配置无法自动生效. func RegisterAutoLoadConfig() { if conf.AutoLoadDelay > 0 { watcher, err := fsnotify.NewWatcher() if err != nil { logs.Error("创建配置文件监控器失败 ->", err) } go func() { for { select { case ev := <-watcher.Event: //如果是修改了配置文件 if ev.IsModify() { if err := web.LoadAppConfig("ini", conf.ConfigurationFile); err != nil { logs.Error("An error occurred ->", err) continue } RegisterCache() RegisterLogger("") logs.Info("配置文件已加载 ->", conf.ConfigurationFile) } else if ev.IsRename() { _ = watcher.WatchFlags(conf.ConfigurationFile, fsnotify.FSN_MODIFY|fsnotify.FSN_RENAME) } logs.Info(ev.String()) case err := <-watcher.Error: logs.Error("配置文件监控器错误 ->", err) } } }() err = watcher.WatchFlags(conf.ConfigurationFile, fsnotify.FSN_MODIFY|fsnotify.FSN_RENAME) if err != nil { logs.Error("监控配置文件失败 ->", err) } } } // 注册错误处理方法. func RegisterError() { web.ErrorHandler("404", func(writer http.ResponseWriter, request *http.Request) { var buf bytes.Buffer data := make(map[string]interface{}) data["ErrorCode"] = 404 data["ErrorMessage"] = "页面未找到或已删除" if err := web.ExecuteViewPathTemplate(&buf, "errors/error.tpl", web.BConfig.WebConfig.ViewsPath, data); err == nil { _, _ = fmt.Fprint(writer, buf.String()) } else { _, _ = fmt.Fprint(writer, data["ErrorMessage"]) } }) web.ErrorHandler("401", func(writer http.ResponseWriter, request *http.Request) { var buf bytes.Buffer data := make(map[string]interface{}) data["ErrorCode"] = 401 data["ErrorMessage"] = "请与 Web 服务器的管理员联系,以确认您是否具有访问所请求资源的权限。" if err := web.ExecuteViewPathTemplate(&buf, "errors/error.tpl", web.BConfig.WebConfig.ViewsPath, data); err == nil { _, _ = fmt.Fprint(writer, buf.String()) } else { _, _ = fmt.Fprint(writer, data["ErrorMessage"]) } }) } func init() { if configPath, err := filepath.Abs(conf.ConfigurationFile); err == nil { conf.ConfigurationFile = configPath } if err := gocaptcha.ReadFonts(conf.WorkingDir("static", "fonts"), ".ttf"); err != nil { log.Fatal("读取字体文件失败 ->", err) } gob.Register(models.Member{}) if p, err := filepath.Abs(os.Args[0]); err == nil { conf.WorkingDirectory = filepath.Dir(p) } } ================================================ FILE: commands/daemon/daemon.go ================================================ package daemon import ( "fmt" "os" "path/filepath" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/kardianos/service" "github.com/mindoc-org/mindoc/commands" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/controllers" ) type Daemon struct { config *service.Config errs chan error } func NewDaemon() *Daemon { config := &service.Config{ Name: "mindocd", //服务显示名称 DisplayName: "MinDoc service", //服务名称 Description: "A document online management program.", //服务描述 WorkingDirectory: conf.WorkingDirectory, Arguments: os.Args[1:], } return &Daemon{ config: config, errs: make(chan error, 100), } } func (d *Daemon) Config() *service.Config { return d.config } func (d *Daemon) Start(s service.Service) error { go d.Run() return nil } func (d *Daemon) Run() { commands.ResolveCommand(d.config.Arguments) commands.RegisterFunction() commands.RegisterAutoLoadConfig() commands.RegisterError() web.ErrorController(&controllers.ErrorController{}) f, err := filepath.Abs(os.Args[0]) if err != nil { f = os.Args[0] } fmt.Printf("MinDoc version => %s\nbuild time => %s\nstart directory => %s\n%s\n", conf.VERSION, conf.BUILD_TIME, f, conf.GO_VERSION) web.Run() } func (d *Daemon) Stop(s service.Service) error { if service.Interactive() { os.Exit(0) } return nil } func Install() { d := NewDaemon() d.config.Arguments = os.Args[3:] s, err := service.New(d, d.config) if err != nil { logs.Error("Create service error => ", err) os.Exit(1) } err = s.Install() if err != nil { logs.Error("Install service error:", err) os.Exit(1) } else { logs.Info("Service installed!") } os.Exit(0) } func Uninstall() { d := NewDaemon() s, err := service.New(d, d.config) if err != nil { logs.Error("Create service error => ", err) os.Exit(1) } err = s.Uninstall() if err != nil { logs.Error("Install service error:", err) os.Exit(1) } else { logs.Info("Service uninstalled!") } os.Exit(0) } func Restart() { d := NewDaemon() s, err := service.New(d, d.config) if err != nil { logs.Error("Create service error => ", err) os.Exit(1) } err = s.Restart() if err != nil { logs.Error("Install service error:", err) os.Exit(1) } else { logs.Info("Service Restart!") } os.Exit(0) } ================================================ FILE: commands/install.go ================================================ package commands import ( "errors" "fmt" "os" "time" "flag" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" ) //系统安装. func Install() { fmt.Println("Initializing...") err := orm.RunSyncdb("default", false, true) if err == nil { initialization() } else { panic(err.Error()) } fmt.Println("Install Successfully!") os.Exit(0) } func Version() { if len(os.Args) >= 2 && os.Args[1] == "version" { fmt.Println(conf.VERSION) os.Exit(0) } } //修改用户密码 func ModifyPassword() { var account, password string //账号和密码需要解析参数后才能获取 if len(os.Args) >= 2 && os.Args[1] == "password" { flagSet := flag.NewFlagSet("MinDoc command: ", flag.ExitOnError) flagSet.StringVar(&account, "account", "", "用户账号.") flagSet.StringVar(&password, "password", "", "用户密码.") if err := flagSet.Parse(os.Args[2:]); err != nil { logs.Error("解析参数失败 -> ", err) os.Exit(1) } if len(os.Args) < 2 { fmt.Println("Parameter error.") os.Exit(1) } if account == "" { fmt.Println("Account cannot be empty.") os.Exit(1) } if password == "" { fmt.Println("Password cannot be empty.") os.Exit(1) } member, err := models.NewMember().FindByAccount(account) if err != nil { fmt.Println("Failed to change password:", err) os.Exit(1) } pwd, err := utils.PasswordHash(password) if err != nil { fmt.Println("Failed to change password:", err) os.Exit(1) } member.Password = pwd err = member.Update("password") if err != nil { fmt.Println("Failed to change password:", err) os.Exit(1) } fmt.Println("Successfully modified.") os.Exit(0) } } //初始化数据 func initialization() { err := models.NewOption().Init() if err != nil { panic(err.Error()) } lang, _ := web.AppConfig.String("default_lang") err = i18n.SetMessage(lang, "conf/lang/"+lang+".ini") if err != nil { panic(fmt.Errorf("initialize locale error: %s", err)) } member, err := models.NewMember().FindByFieldFirst("account", "admin") if errors.Is(err, orm.ErrNoRows) { // create admin user logs.Info("creating admin user") member.Account = "admin" member.Avatar = conf.URLForWithCdnImage("/static/images/headimgurl.jpg") member.Password = "123456" member.AuthMethod = "local" member.Role = conf.MemberSuperRole member.Email = "admin@iminho.me" if err := member.Add(); err != nil { panic("Member.Add => " + err.Error()) } // create demo book logs.Info("creating demo book") book := models.NewBook() book.MemberId = member.MemberId book.BookName = i18n.Tr(lang, "init.default_proj_name") //"MinDoc演示项目" book.Status = 0 book.ItemId = 1 book.Description = i18n.Tr(lang, "init.default_proj_desc") //"这是一个MinDoc演示项目,该项目是由系统初始化时自动创建。" book.CommentCount = 0 book.PrivatelyOwned = 0 book.CommentStatus = "closed" book.Identify = "mindoc" book.DocCount = 0 book.CommentCount = 0 book.Version = time.Now().Unix() book.Cover = conf.GetDefaultCover() book.Editor = "markdown" book.Theme = "default" if err := book.Insert(lang); err != nil { panic("初始化项目失败 -> " + err.Error()) } } else if err != nil { panic(fmt.Errorf("occur errors when initialize: %s", err)) } if !models.NewItemsets().Exist(1) { item := models.NewItemsets() item.ItemName = i18n.Tr(lang, "init.default_proj_space") //"默认项目空间" item.MemberId = 1 if err := item.Save(); err != nil { panic("初始化项目空间失败 -> " + err.Error()) } } } ================================================ FILE: commands/migrate/migrate.go ================================================ // Copyright 2013 bee authors // // 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. package migrate import ( "os" "container/list" "fmt" "log" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/server/web" "github.com/mindoc-org/mindoc/models" ) var ( migrationList = &migrationCache{} ) type MigrationDatabase interface { //获取当前的版本 Version() int64 //校验当前是否可更新 ValidUpdate(version int64) error //校验并备份表结构 ValidForBackupTableSchema() error //校验并更新表结构 ValidForUpdateTableSchema() error //恢复旧数据 MigrationOldTableData() error //插入新数据 MigrationNewTableData() error //增加迁移记录 AddMigrationRecord(version int64) error //最后的清理工作 MigrationCleanup() error //回滚本次迁移 RollbackMigration() error } type migrationCache struct { items *list.List } func RunMigration() { if len(os.Args) >= 2 && os.Args[1] == "migrate" { migrate, err := models.NewMigration().FindFirst() if err != nil { //log.Fatalf("migrations table %s", err) migrate = models.NewMigration() } fmt.Println("Start migration databae... ") for el := migrationList.items.Front(); el != nil; el = el.Next() { //如果存在比当前版本大的版本,则依次升级 if item, ok := el.Value.(MigrationDatabase); ok && item.Version() > migrate.Version { err := item.ValidUpdate(migrate.Version) if err != nil { log.Fatal(err) } err = item.ValidForBackupTableSchema() if err != nil { item.RollbackMigration() log.Fatal(err) } err = item.ValidForUpdateTableSchema() if err != nil { item.RollbackMigration() log.Fatal(err) } err = item.MigrationOldTableData() if err != nil { item.RollbackMigration() log.Fatal(err) } err = item.MigrationNewTableData() if err != nil { item.RollbackMigration() log.Fatal(err) } err = item.AddMigrationRecord(item.Version()) if err != nil { item.RollbackMigration() log.Fatal(err) } err = item.MigrationCleanup() if err != nil { item.RollbackMigration() log.Fatal(err) } } } fmt.Println("Migration successfull.") os.Exit(0) } } //导出数据库的表结构 func ExportDatabaseTable() ([]string, error) { dbadapter, _ := web.AppConfig.String("db_adapter") dbdatabase, _ := web.AppConfig.String("db_database") tables := make([]string, 0) o := orm.NewOrm() switch dbadapter { case "mysql": { var lists []orm.Params _, err := o.Raw(fmt.Sprintf("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '%s'", dbdatabase)).Values(&lists) if err != nil { return tables, err } for _, table := range lists { var results []orm.Params _, err = o.Raw(fmt.Sprintf("show create table %s", table["TABLE_NAME"])).Values(&results) if err != nil { return tables, err } tables = append(tables, results[0]["Create Table"].(string)) } break } case "sqlite3": { var results []orm.Params _, err := o.Raw("SELECT sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY rootpage ASC").Values(&results) if err != nil { return tables, err } for _, item := range results { if sql, ok := item["sql"]; ok { tables = append(tables, sql.(string)) } } break } } return tables, nil } func RegisterMigration() { migrationList.items = list.New() migrationList.items.PushBack(NewMigrationVersion03()) } ================================================ FILE: commands/migrate/migrate_v03.go ================================================ package migrate import ( "errors" "fmt" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/models" ) type MigrationVersion03 struct { isValid bool tables []string } func NewMigrationVersion03() *MigrationVersion03 { return &MigrationVersion03{isValid: false, tables: make([]string, 0)} } func (m *MigrationVersion03) Version() int64 { return 201705271114 } func (m *MigrationVersion03) ValidUpdate(version int64) error { if m.Version() > version { m.isValid = true return nil } m.isValid = false return errors.New("The target version is higher than the current version.") } func (m *MigrationVersion03) ValidForBackupTableSchema() error { if !m.isValid { return errors.New("The current version failed to verify.") } var err error m.tables, err = ExportDatabaseTable() return err } func (m *MigrationVersion03) ValidForUpdateTableSchema() error { if !m.isValid { return errors.New("The current version failed to verify.") } err := orm.RunSyncdb("default", false, true) if err != nil { return err } //_,err = o.Raw("ALTER TABLE md_members ADD auth_method VARCHAR(50) DEFAULT 'local' NULL").Exec() return err } func (m *MigrationVersion03) MigrationOldTableData() error { if !m.isValid { return errors.New("The current version failed to verify.") } return nil } func (m *MigrationVersion03) MigrationNewTableData() error { if !m.isValid { return errors.New("The current version failed to verify.") } o := orm.NewOrm() _, err := o.Raw("UPDATE md_members SET auth_method = 'local'").Exec() if err != nil { return err } _, err = o.Raw("INSERT INTO md_options (option_title, option_name, option_value) SELECT '是否启用文档历史','ENABLE_DOCUMENT_HISTORY','true' WHERE NOT exists(SELECT * FROM md_options WHERE option_name = 'ENABLE_DOCUMENT_HISTORY');").Exec() if err != nil { return err } return nil } func (m *MigrationVersion03) AddMigrationRecord(version int64) error { o := orm.NewOrm() tables, err := ExportDatabaseTable() if err != nil { return err } migration := models.NewMigration() migration.Version = version migration.Status = "update" migration.CreateTime = time.Now() migration.Name = fmt.Sprintf("update_%d", version) migration.Statements = strings.Join(tables, "\r\n") _, err = o.Insert(migration) return err } func (m *MigrationVersion03) MigrationCleanup() error { return nil } func (m *MigrationVersion03) RollbackMigration() error { if !m.isValid { return errors.New("The current version failed to verify.") } o := orm.NewOrm() _, err := o.Raw("ALTER TABLE md_members DROP COLUMN auth_method").Exec() if err != nil { return err } _, err = o.Raw("DROP TABLE md_document_history").Exec() if err != nil { return err } _, err = o.Raw("DELETE md_options WHERE option_name = 'ENABLE_DOCUMENT_HISTORY'").Exec() if err != nil { return err } return nil } ================================================ FILE: commands/update.go ================================================ package commands import ( "encoding/json" "fmt" "github.com/mindoc-org/mindoc/models" "io/ioutil" "net/http" "os" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) // 检查最新版本. func CheckUpdate() { fmt.Println("MinDoc current version => ", conf.VERSION) resp, err := http.Get("https://api.github.com/repos/mindoc-org/mindoc/tags") if err != nil { logs.Error("CheckUpdate => ", err) os.Exit(1) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { logs.Error("CheckUpdate => ", err) os.Exit(1) } var result []*struct { Name string `json:"name"` } err = json.Unmarshal(body, &result) if err != nil { logs.Error("CheckUpdate => ", err) os.Exit(0) } if len(result) > 0 { fmt.Println("MinDoc last version => ", result[0].Name) } os.Exit(0) } func Update() { fmt.Println("Update...") RegisterDataBase() RegisterModel() err := orm.RunSyncdb("default", false, true) if err == nil { UpdateInitialization() } else { panic(err.Error()) } fmt.Println("Update Successfully!") os.Exit(0) } func UpdateInitialization() { err := models.NewOption().Update() if err != nil { panic(err.Error()) } } ================================================ FILE: conf/app.conf.example ================================================ appname = mindoc #默认监听的网卡,为空则监听所有 httpaddr="${MINDOC_ADDR}" httpport = "${MINDOC_PORT||8181}" runmode = "${MINDOC_RUN_MODE||dev}" sessionon = true sessionname = mindoc_id copyrequestbody = true enablexsrf = "${MINDOC_ENABLE_XSRF||false}" enable_iframe = "${MINDOC_ENABLE_IFRAME||false}" #系统完整URL(http://doc.iminho.me),如果该项不设置,会从请求头中获取地址。 baseurl="${MINDOC_BASE_URL}" #########代码高亮样式################ #样式演示地址:https://highlightjs.org/static/demo/ highlight_style="${MINDOC_HIGHLIGHT_STYLE||github}" ########配置文件自动加载################## #大于0时系统会自动检测配置文件是否变动,变动后自动加载并生效,单位是秒。监听端口和数据库配置无效 config_auto_delay="${MINDOC_CONFIG_AUTO_DELAY||20}" #发布pdf时候的默认发布者(项目填写了公司名称以公司名称为准) publisher_def = ########Session储存方式############## #默认Session生成Key的秘钥 beegoserversessionkey=NY1B$28pms12JM&c sessionprovider="${MINDOC_SESSION_PROVIDER||file}" sessionproviderconfig="${MINDOC_SESSION_PROVIDER_CONFIG||./runtime/session}" #默认的过期时间 sessiongcmaxlifetime="${MINDOC_SESSION_MAX_LIFETIME||3600}" #以文件方式储存 #sessionprovider=file #sessionproviderconfig=./runtime/session #以redis方式储存 #sessionprovider=redis #sessionproviderconfig=127.0.0.1:6379 #以memcache方式储存 #sessionprovider=memcache #sessionproviderconfig=127.0.0.1:11211 #以内存方式托管Session #sessionprovider=memory #时区设置 timezone = Asia/Shanghai ####################MySQL 数据库配置########################### #支持MySQL,sqlite3,postgres三种数据库,如果是sqlite3 则 db_database 标识数据库的物理目录 db_adapter="${MINDOC_DB_ADAPTER||sqlite3}" db_host="${MINDOC_DB_HOST||127.0.0.1}" db_port="${MINDOC_DB_PORT||3306}" db_database="${MINDOC_DB_DATABASE||./database/mindoc.db}" db_username="${MINDOC_DB_USERNAME||root}" db_password="${MINDOC_DB_PASSWORD||123456}" #是否使用SSL,支持posgres,可选的值有: #disable - No SSL #require - Always SSL (skip verification) #verify-ca - Always SSL (verify that the certificate presented by the server was signed by a trusted CA) #verify-full - Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate) db_sslmode="${MINDOC_DB_SSLMODE||disable}" ####################sqlite3 数据库配置########################### #db_adapter=sqlite3 #db_database=./database/mindoc.db #项目默认封面 cover=/static/images/book.jpg #默认头像 avatar=/static/images/headimgurl.jpg #默认阅读令牌长度 token_size=12 #上传文件的后缀,如果不限制后缀可以设置为 * upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif|mp4|webm|avi #上传的文件大小限制 # - 如果不填写, 则默认1GB,如果希望超过1GB,必须带单位 # - 如果填写,单位可以是 TB、GB、MB、KB,不带单位表示字节 upload_file_size=10MB ####################邮件配置###################### #是否启用邮件 enable_mail="${MINDOC_ENABLE_MAIL||false}" #每小时限制指定邮箱邮件发送次数 mail_number="${MINDOC_MAIL_NUMBER||5}" #smtp服务用户名 smtp_user_name="${MINDOC_SMTP_USER_NAME||admin@iminho.me}" #smtp服务器地址 smtp_host="${MINDOC_SMTP_HOST||smtp.163.com}"" #smtp密码 smtp_password="${MINDOC_SMTP_PASSWORD}" #端口号 smtp_port="${MINDOC_SMTP_PORT||25}"" #发送邮件的显示名称 form_user_name="${MINDOC_FORM_USERNAME||admin@iminho.me}" #邮件有效期30分钟 mail_expired="${MINDOC_EXPIRED||30}" #加密类型NONE 无认证、SSL 加密、LOGIN 普通用户登录 secure="${MINDOC_MAIL_SECURE||LOGIN}" ###############配置导出项目################### enable_export="${MINDOC_ENABLE_EXPORT||false}" #同一个项目同时运行导出程序的并行数量,取值1-4之间,取值越大导出速度越快,越占用资源 export_process_num="${MINDOC_EXPORT_PROCESS_NUM||1}" #并发导出的项目限制,指同一时间限制的导出项目数量,如果为0则不限制。设置的越大,越占用资源 export_limit_num="${MINDOC_EXPORT_LIMIT_NUM||5}" #指同时等待导出的任务数量 export_queue_limit_num="${MINDOC_EXPORT_QUEUE_LIMIT_NUM||100}" #导出项目的缓存目录配置 export_output_path="${MINDOC_EXPORT_OUTPUT_PATH||./runtime/cache}" ################百度地图密钥################# baidumapkey= ################Active Directory/LDAP################ #是否启用ldap ldap_enable=${MINDOC_LDAP_ENABLE||false} #ldap协议(ldap/ldaps) ldap_scheme="${MINDOC_LDAP_SCHEME||ldap}" #ldap主机名 ldap_host="${MINDOC_LDAP_HOST||127.0.0.1}" #ldap端口 ldap_port=${MINDOC_LDAP_PORT||389} #ldap内哪个属性作为用户名 ldap_account="${MINDOC_LDAP_ACCOUNT||sAMAccountName}" #ldap内哪个属性作为邮箱 ldap_mail="${MINDOC_LDAP_MAIL||mail}" #搜索范围 ldap_base="${MINDOC_LDAP_BASE||dc=example,dc=com}" #第一次绑定ldap用户dn ldap_user="${MINDOC_LDAP_USER||cn=ldap helper,ou=example.com,dc=example,dc=com}" #第一次绑定ldap用户密码 ldap_password="${MINDOC_LDAP_PASSWORD||xxx}" #自动注册用户角色:0 超级管理员 /1 管理员/ 2 普通用户/ 3 只读用户 ldap_user_role=${MINDOC_LDAP_USER_ROLE||2} #ldap搜索filter规则,AD服务器: objectClass=User, openldap服务器: objectClass=posixAccount ,也可以定义为其他属性,如: title=mindoc ldap_filter="${MINDOC_LDAP_FILTER||objectClass=posixAccount}" ############# HTTP自定义接口登录 ################ http_login_url= #md5计算的秘钥 http_login_secret=hzsp*THJUqwbCU%s ################################## ###############配置CDN加速################## cdn="${MINDOC_CDN_URL}" cdnjs="${MINDOC_CDN_JS_URL}" cdncss="${MINDOC_CDN_CSS_URL}" cdnimg="${MINDOC_CDN_IMG_URL}" ######################缓存配置############################### #是否开启缓存,true 开启/false 不开启 cache="${MINDOC_CACHE||false}" #缓存方式:memory/memcache/redis/file cache_provider="${MINDOC_CACHE_PROVIDER||file}" #当配置缓存方式为memory时,内存回收时间,单位是秒 cache_memory_interval="${MINDOC_CACHE_MEMORY_INTERVAL||120}" #当缓存方式配置为file时,缓存的储存目录 cache_file_path="${MINDOC_CACHE_FILE_PATH||./runtime/cache/}" #缓存文件后缀 cache_file_suffix="${MINDOC_CACHE_FILE_SUFFIX||.bin}" #文件缓存目录层级 cache_file_dir_level="${MINDOC_CACHE_FILE_DIR_LEVEL||2}" #文件缓存的默认过期时间 cache_file_expiry="${MINDOC_CACHE_FILE_EXPIRY||3600}" #memcache缓存服务器地址 cache_memcache_host="${MINDOC_CACHE_MEMCACHE_HOST||127.0.0.1:11211}" #redis服务器地址 cache_redis_host="${MINDOC_CACHE_REDIS_HOST||127.0.0.1:6379}" #redis数据库索引 cache_redis_db="${MINDOC_CACHE_REDIS_DB||0}" #redis服务器密码 cache_redis_password="${MINDOC_CACHE_REDIS_PASSWORD}" #缓存键的前缀 cache_redis_prefix="${MINDOC_CACHE_REDIS_PREFIX||mindoc::cache}" #########日志储存配置############## #日志保存路径,在linux上,自动创建的日志文件请不要删除,否则将无法写入日志 log_path="${MINDOC_LOG_PATH||./runtime/logs}" #每个文件保存的最大行数,默认值 1000000 log_maxlines="${MINDOC_LOG_MAX_LINES||1000000}" # 每个文件保存的最大尺寸,默认值是 1 << 28, //256 MB log_maxsize="${MINDOC_LOG_MAX_SIZE}" # 是否按照每天 logrotate,默认是 true log_daily="${MINDOC_LOG_DAILY||true}" # 文件最多保存多少天,默认保存 7 天 log_maxdays="${MINDOC_LOG_MAX_DAYS||30}" # 日志保存的时候的级别,默认是 Trace 级别,可选值: Emergency/Alert/Critical/Error/Warning/Notice/Informational/Debug/Trace log_level="${MINDOC_LOG_LEVEL||Alert}" # 是否异步生成日志,默认是 true log_is_async="${MINDOC_LOG_IS_ASYNC||TRUE}" ##########钉钉应用相关配置############## # 企业钉钉ID dingtalk_corpid="${MINDOC_DINGTALK_CORPID}" # 钉钉AppKey dingtalk_app_key="${MINDOC_DINGTALK_APPKEY}" # 钉钉AppSecret dingtalk_app_secret="${MINDOC_DINGTALK_APPSECRET}" ########企业微信登录配置############## # 企业ID workweixin_corpid="${MINDOC_WORKWEIXIN_CORPID}" # 应用ID workweixin_agentid="${MINDOC_WORKWEIXIN_AGENTID}" # 应用密钥 workweixin_secret="${MINDOC_WORKWEIXIN_SECRET}" # i18n config i18n_list=zh-cn:简体中文|en-us:English|ru-ru:Русский default_lang="zh-cn" # MCP Server 功能 enable_mcp_server="${MINDOC_ENABLE_MCP_SERVER||false}" mcp_api_key="${MINDOC_MCP_API_KEY||demo-mcp-api-key}" ================================================ FILE: conf/enumerate.go ================================================ // package conf 为配置相关. package conf import ( "strings" "fmt" "os" "path/filepath" "strconv" "github.com/beego/beego/v2/server/web" ) // 登录用户的Session名 const LoginSessionName = "LoginSessionName" const CaptchaSessionName = "__captcha__" const RegexpEmail = "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" // 允许用户名中出现点号 const RegexpAccount = `^[a-zA-Z0-9][a-zA-Z0-9\.-]{2,50}$` // PageSize 默认分页条数. const PageSize = 10 // 用户权限 const ( // 超级管理员. MemberSuperRole SystemRole = iota //普通管理员. MemberAdminRole //普通用户. MemberGeneralRole //只读用户. MemberReaderRole ) // 系统角色 type SystemRole int const ( // 创始人. BookFounder BookRole = iota //管理者 BookAdmin //编辑者. BookEditor //观察者 BookObserver //未指定关系 BookRoleNoSpecific ) // 项目角色 type BookRole int const ( LoggerOperate = "operate" LoggerSystem = "system" LoggerException = "exception" LoggerDocument = "document" ) const ( //本地账户校验 AuthMethodLocal = "local" //LDAP用户校验 AuthMethodLDAP = "ldap" ) var ( VERSION string BUILD_TIME string GO_VERSION string ) var ( ConfigurationFile = "./conf/app.conf" WorkingDirectory = "./" LogFile = "./runtime/logs" BaseUrl = "" AutoLoadDelay = 0 ) // app_key func GetAppKey() string { return web.AppConfig.DefaultString("app_key", "mindoc") } func GetDatabasePrefix() string { return web.AppConfig.DefaultString("db_prefix", "md_") } // 获取默认头像 func GetDefaultAvatar() string { return URLForWithCdnImage(web.AppConfig.DefaultString("avatar", "/static/images/headimgurl.jpg")) } // 获取阅读令牌长度. func GetTokenSize() int { return web.AppConfig.DefaultInt("token_size", 12) } // 获取默认文档封面. func GetDefaultCover() string { return URLForWithCdnImage(web.AppConfig.DefaultString("cover", "/static/images/book.jpg")) } // 获取允许的上传文件的类型. func GetUploadFileExt() []string { ext := web.AppConfig.DefaultString("upload_file_ext", "png|jpg|jpeg|gif|txt|doc|docx|pdf|mp4") temp := strings.Split(ext, "|") exts := make([]string, len(temp)) i := 0 for _, item := range temp { if item != "" { exts[i] = item i++ } } return exts } // 获取上传文件允许的最大值 func GetUploadFileSize() int64 { size := web.AppConfig.DefaultString("upload_file_size", "0") if strings.HasSuffix(size, "TB") { if s, e := strconv.ParseInt(size[0:len(size)-2], 10, 64); e == nil { return s * 1024 * 1024 * 1024 * 1024 } } if strings.HasSuffix(size, "GB") { if s, e := strconv.ParseInt(size[0:len(size)-2], 10, 64); e == nil { return s * 1024 * 1024 * 1024 } } if strings.HasSuffix(size, "MB") { if s, e := strconv.ParseInt(size[0:len(size)-2], 10, 64); e == nil { return s * 1024 * 1024 } } if strings.HasSuffix(size, "KB") { if s, e := strconv.ParseInt(size[0:len(size)-2], 10, 64); e == nil { return s * 1024 } } if s, e := strconv.ParseInt(size, 10, 64); e == nil { return s } return 0 } // 是否启用导出 func GetEnableExport() bool { return web.AppConfig.DefaultBool("enable_export", true) } // 是否启用iframe func GetEnableIframe() bool { return web.AppConfig.DefaultBool("enable_iframe", false) } // 同一项目导出线程的并发数 func GetExportProcessNum() int { exportProcessNum := web.AppConfig.DefaultInt("export_process_num", 1) if exportProcessNum <= 0 || exportProcessNum > 4 { exportProcessNum = 1 } return exportProcessNum } // 导出项目队列的并发数量 func GetExportLimitNum() int { exportLimitNum := web.AppConfig.DefaultInt("export_limit_num", 1) if exportLimitNum < 0 { exportLimitNum = 1 } return exportLimitNum } // 等待导出队列的长度 func GetExportQueueLimitNum() int { exportQueueLimitNum := web.AppConfig.DefaultInt("export_queue_limit_num", 10) if exportQueueLimitNum <= 0 { exportQueueLimitNum = 100 } return exportQueueLimitNum } // 默认导出项目的缓存目录 func GetExportOutputPath() string { exportOutputPath := filepath.Join(web.AppConfig.DefaultString("export_output_path", filepath.Join(WorkingDirectory, "cache")), "books") return exportOutputPath } // 判断是否是允许上传的文件类型. func IsAllowUploadFileExt(ext string) bool { if strings.HasPrefix(ext, ".") { ext = string(ext[1:]) } exts := GetUploadFileExt() for _, item := range exts { if item == "*" { return true } if strings.EqualFold(item, ext) { return true } } return false } // 读取配置文件值 func CONF(key string, value ...string) string { defaultValue := "" if len(value) > 0 { defaultValue = value[0] } return web.AppConfig.DefaultString(key, defaultValue) } // 重写生成URL的方法,加上完整的域名 func URLFor(endpoint string, values ...interface{}) string { baseUrl := web.AppConfig.DefaultString("baseurl", "") pathUrl := web.URLFor(endpoint, values...) if baseUrl == "" { baseUrl = BaseUrl } if strings.HasPrefix(pathUrl, "http://") { return pathUrl } if strings.HasPrefix(pathUrl, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + pathUrl[1:] } if !strings.HasPrefix(pathUrl, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + pathUrl } return baseUrl + web.URLFor(endpoint, values...) } func URLForNotHost(endpoint string, values ...interface{}) string { baseUrl := web.AppConfig.DefaultString("baseurl", "") pathUrl := web.URLFor(endpoint, values...) if baseUrl == "" { baseUrl = "/" } if strings.HasPrefix(pathUrl, "http://") { return pathUrl } if strings.HasPrefix(pathUrl, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + pathUrl[1:] } if !strings.HasPrefix(pathUrl, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + pathUrl } return baseUrl + web.URLFor(endpoint, values...) } func URLForWithCdnImage(p string) string { if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") { return p } cdn := web.AppConfig.DefaultString("cdnimg", "") //如果没有设置cdn,则使用baseURL拼接 if cdn == "" { baseUrl := web.AppConfig.DefaultString("baseurl", "/") if strings.HasPrefix(p, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + p[1:] } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + p } return baseUrl + p } if strings.HasPrefix(p, "/") && strings.HasSuffix(cdn, "/") { return cdn + string(p[1:]) } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(cdn, "/") { return cdn + "/" + p } return cdn + p } func URLForWithCdnCss(p string, v ...string) string { cdn := web.AppConfig.DefaultString("cdncss", "") if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") { return p } filePath := WorkingDir(p) if f, err := os.Stat(filePath); err == nil && !strings.Contains(p, "?") && len(v) > 0 && v[0] == "version" { p = p + fmt.Sprintf("?v=%s", f.ModTime().Format("20060102150405")) } //如果没有设置cdn,则使用baseURL拼接 if cdn == "" { baseUrl := web.AppConfig.DefaultString("baseurl", "/") if strings.HasPrefix(p, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + p[1:] } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + p } return baseUrl + p } if strings.HasPrefix(p, "/") && strings.HasSuffix(cdn, "/") { return cdn + string(p[1:]) } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(cdn, "/") { return cdn + "/" + p } return cdn + p } func URLForWithCdnJs(p string, v ...string) string { cdn := web.AppConfig.DefaultString("cdnjs", "") if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") { return p } filePath := WorkingDir(p) if f, err := os.Stat(filePath); err == nil && !strings.Contains(p, "?") && len(v) > 0 && v[0] == "version" { p = p + fmt.Sprintf("?v=%s", f.ModTime().Format("20060102150405")) } //如果没有设置cdn,则使用baseURL拼接 if cdn == "" { baseUrl := web.AppConfig.DefaultString("baseurl", "/") if strings.HasPrefix(p, "/") && strings.HasSuffix(baseUrl, "/") { return baseUrl + p[1:] } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(baseUrl, "/") { return baseUrl + "/" + p } return baseUrl + p } if strings.HasPrefix(p, "/") && strings.HasSuffix(cdn, "/") { return cdn + string(p[1:]) } if !strings.HasPrefix(p, "/") && !strings.HasSuffix(cdn, "/") { return cdn + "/" + p } return cdn + p } func WorkingDir(elem ...string) string { elems := append([]string{WorkingDirectory}, elem...) return filepath.Join(elems...) } func init() { if p, err := filepath.Abs("./conf/app.conf"); err == nil { ConfigurationFile = p } if p, err := filepath.Abs("./"); err == nil { WorkingDirectory = p } if p, err := filepath.Abs("./runtime/logs"); err == nil { LogFile = p } } ================================================ FILE: conf/lang/en-us.ini ================================================ [common] title = mindoc home = Home blog = Blog project_space = Project Space person_center = Personal Center my_project = My Project my_blog = My Article manage = Management login = Log In logout = Log Out official_website = Official Website feedback = Feedback source_code = Source Code manual = Manual username = Username account = Account email = Email password = Password role = Role captcha = Captcha keep_login = Stay signed in forgot_password = Forgot password? register = Create New Account third_party_login = Third Party Login dingtalk_login = DingTalk Login wecom_login = WeCom Login account_recovery = Account recovery new_password = New password confirm_password = Confirm password new_account = Create New Account setting = Setting save = Save edit = Edit delete = Delete cancel = Cancel create = Create confirm_delete = Confirm upload_lang = en js_lang = en remove = Remove operate = Operate confirm = Confirm creator = Creator administrator = Administrator editor = Editor observer = Observer back = Back detail = Detail admin_right = Reading, writing and management editor_right = Reading and writing observer_right = Reading only yes = yes no = no read = Read generate = Generate clean = Clean [init] default_proj_name = MinDoc Demo Project default_proj_desc = This is a MinDoc demo project, which is automatically created when the system is initialized. default_proj_space = Default Project Space blank_doc = Blank document [message] tips = Tips page_not_existed = The page does not exist no_permission = No enough permissions keyword_placeholder = input keyword please... wrong_account_password = Incorrect username or password wrong_password = Wrong password click_to_change = Click to change one logging_in = logging in... need_relogin = Relogin please. return_account_login = Return account password login no_account_yet = No account yet? has_account = Already have an account? account_empty = Account cannot be empty email_empty = Email cannot be empty password_empty = Password cannot be empty captcha_empty = Captcha cannot be empty system_error = System error processing = Processing... email_sent = The email is sent successfully, please log in to check it. confirm_password_empty = Confirm password cannot be empty incorrect_confirm_password = Incorrect confirm password illegal_request = Illegal request account_or_password_empty = Account or Password cannot be empty captcha_wrong = Incorrect captcha password_length_invalid = The password cannot be empty and must be between 6-50 characters mail_expired = Mail has expired captcha_expired = The verification code has expired, please try again. user_not_existed = User does not exist readusr_only_observer = Read only users can only be set as observers email_not_exist = Email does not exist failed_save_password = Failed to save password mail_service_not_enable = Mail service is not enabled account_disable = Account has been disabled failed_send_mail = Failed to send mail sent_too_many_times = Send too many times, please try again later account_not_support_retrieval = The current user does not support password retrieval username_invalid_format = The account number can only be composed of English alphanumerics and 3-50 characters email_invalid_format = Email format is incorrect account_existed = Username already existed failed_register = Registration failed, please contact the system administrator failed_obtain_user_info = Failed to obtain identity information dingtalk_auto_login_not_enable = DingTalk automatic login function is not enabled failed_auto_login = Automatic login failed no_project = No Project item_not_exist = Item does not exist or has been deleted item_not_exist_or_no_permit = Item does not exist or has insufficient permissions doc_not_exist = Document does not exist or has been deleted doc_not_exist_or_no_permit = Document does not exist or has insufficient permissions unknown_exception = Unknown Exception no_data = No Data project_must_belong_space = Project must belong to a project space, and the administrator can manage it project_title_placeholder = Title (limit in 30 words) project_title_tips = Project name cannot exceed 100 characters project_id_placeholder = Project ID (limit in 30 characters) project_id_tips = The ID can only contain lowercase letters, numbers, and "-", "." and "_" symbols. project_desc_placeholder = Project description cannot exceed 500 characters project_public_desc = (Anyone can access) project_private_desc = (Only participants or use tokens can access) project_cover_desc = The Cover can be edit in the settings confirm_delete_project = Are you sure you want to delete the project? warning_delete_project = Remove items will not be retrieved. project_space_empty = Please select project space project_title_empty = Project title cannot be empty project_id_empty = Project ID cannot be empty project_id_existed = Project ID already in use project_id_error = Project ID error project_id_length = Project ID must be less than 50 characters import_file_empty = Please select the file to upload file_type_placeholder = Please select a Zip file publish_to_queue = The publish task has been pushed to the task queue and will be executed soon. team_name_empty = Team name cannot be empty operate_failed = Operation failed project_id_desc = The project ID is used to mark the uniqueness of the item and cannot be modified. history_record_amount_desc = When document history enabled, this value limits the number of history saved per document corp_id_desc = The footer that appears when the document PDF document is exported project_desc_desc = The description information is no more than 500 characters, supports markdown syntax project_desc_tips = The description information is no more than 500 characters access_pass_desc = The password that needs to be provided to access the project without permit auto_publish_desc = Enable for each save is automatically published to the latest version enable_export_desc = Configure the exporter before you start the export, also enable the export function in the profile enable_share_desc = Sharing is only available for public projects, and private projects do not support sharing auto_save_desc = Save automatically every 30 seconds confirm_into_private = Are you sure you want to make your project private? into_private_notice = Private project need provide access token confirm_into_public = Are you sure you want to make your project public? into_public_notice = Public project could be visit by anyone project_name_empty = Project name cannot be empty success = Success failed = Failed receive_account_empty = The recipient account cannot be empty receive_account_not_exist = The recipient account not exist receive_account_disabled = The recipient account disable cannot_preview = Cannot preview upload_failed = Upload failed upload_file_size_limit = The file must be less than 2MB upload_file_empty = Upload file is empty uploda_file_type_error = upload file type is wrong choose_pic_file = Please select a picture no_doc_in_cur_proj = No documents for the current project build_doc_tree_error = There was an error building the project document tree param_error = Parameter error doc_name_empty = Document name cannot empty parent_id_not_existed = Parent ID not existed doc_not_belong_project = The document does not belong to the specified project` attachment_not_exist = Attachment does not exist read_file_error = Load file error confirm_override_doc = The document has been modified, are you sure to override it? doc_auto_published = The document was automatically published export_func_disable = The export function is disable cur_project_export_func_disable = The export function is disable for current Project cur_project_not_support_md = The Markdown editor is not supported for the current project export_failed = The export failed, check the system logs file_converting = The document is being converted in the background, please download it later unsupport_file_type = Unsupport file type no_exportable_file = The project has no exportable file gen_qrcode_failed = Generate QrCode failed search_result_error = Search error get_doc_his_failed = Fail to get document history project_space_not_exist = Project space does not exist search_placeholder = input keyword please no_search_result = No search results! user_exist_in_proj = The user already exists in the project cannot_change_own_priv = Cannot change own permissions cannot_delete_self = Cannot delete myself cannot_handover_myself = Cannot handover to myself confirm_delete_blog = Confirm delete blog? delete_blog_tips = Deleted blog cannot be retrieved. input_proj_id_pls = input project ID please input_doc_id_pls = input document ID please blog_digest_tips = blog digest cannot exceed 500 characters blog_title_empty = blog title cannot be empty blog_not_exist = Blog does not exist or has been deleted blog_pwd_incorrect = Access password incorrect set_pwd_pls = please set password for encrypt blog unknown_blog_type = unknown blog type blog_title_tips = blog title cannot exceed 200 characters ref_doc_prj_not_existed = The Project of reference document not existed query_ref_doc_error = query reference document failed ref_doc_not_exist_or_no_permit = reference document does not exist or has insufficient permissions blog_id_existed = blog id already existed query_failed = query failed blog_has_modified = The article has been modified cur_user_cannot_change_pwd = The current user does not support changing the password origin_pwd_empty = The origin password cannot be empty new_pwd_empty = The new password cannot be empty confirm_pwd_empty = The confirm password cannot be empty pwd_length = Password must be between 6-18 characters pwd_length_tips = Password must be between 6-50 characters wrong_origin_pwd = The origin password incorrect wrong_confirm_pwd = The confirm passwrod incorrect same_pwd = The new password must different from the origin pwd_encrypt_failed = Password encryption failed team_name_empty = Team name cannot be emtpy proj_empty = Project cannot be empty site_name_empty = Site name cannot be empty proj_space_name_empty = Project space name cannot be empty proj_space_id_empty = Project space id cannot be empty proj_space_id_tips = The project space id can only consist of letters and numbers and be between 2-100 characters project_order_desc = Number only, sort from largest to smallest project_label_desc = Allows up to 10 labels, use ";" to separate multiple tags cannot_change_own_status = Cannot change own status cannot_change_super_status = Cannot change super administrator status cannot_change_super_priv = Cannot change super administrator permissions editors_not_compatible = two editors are not compatible [blog] author = Author project_list = Project List add_project = Add Project import_project = Import Project delete_project = Delete Project project_summary = Project summary read = Read edit = Edit delete = Delete copy = Copy view = View publish = Publish edit_doc = Edit Document default_cover = Default Cover create_time = Create Time update_time = Update Time creator = Creator doc_amount = Number of documents doc_unit = project_role = Project Role last_edit = Last Edited project_title = Project Title project_id = Project ID project_desc = Project description public = Public private = Private public_project = Public Project private_project = Private Project summary = Summary member = Member team = Team comment_amount = Number of comments comment_unit = member_manage = Member Manage add_member = Add Member administrator = Administrator editor = Editor observer = Observer team_manage = Team Manage add_team = Add Team team_name = Team name member_amount = Number of members join_time = Join Time project_setting = Project setting handover_project = Handover make_public = Into Public make_private = Into Privete history_record_amount = Number of history records corp_id = corp name text_editor = editor project_label = Project Label project_order = Project Order access_pass = Access Password access_token = Access Token auto_publish = Auto publish enable_export = Enable Export enable_share = Enable Share set_first_as_home = Set the first document as the default homepage auto_save = Auto Save cover = Cover click_change_cover = Click to change the cover change_cover = Change Cover preview = Preview choose = Choose upload = Upload recipient_account = Recipient blog_list = Blog List add_blog = Add Blog encryption = encryption encrypt = encrypt edit_blog = Edit blog delete_blog = Delete blog setting_blog = Setting Blog no_blog = No Blog blog_setting = Blog Setting title = Blog Title type = Blog Type normal_blog = Normal Blog link_blog = Link Blog ref_doc = Reference Document blog_status = Blog Status blog_pwd = Blog Password blog_digest = Blog Digest posted_on = Posted on modified_on = Modified on prev = prev next = next no = no edit_title = Edit Blog private_blog_tips = Private blog, please enter password to access print_text = Enable Printing [doc] word_to_html = Word to HTML html_to_markdown = HTML to Markdown modify_doc = Modify Document comparison = Comparison save_merge = Save Merge prev_diff = Previous difference next_diff = Next difference merge_to_left = Merge to left merge_to_right = Merge to right exchange_left_right = Exchange left and right print = Print download = Download share = Share share_project = Share project share_url = Project URL contents = Contents search = Search expand = Unfold fold = Fold close = Close doc_publish_by = Document is Published by doc_publish = edit_doc = Edit Document backward = backward save = save save_as_tpl = save as template undo = undo redo = redo bold = bold italic = italic strikethrough = strikethrough h1 = head 1 h2 = head 2 h3 = head 3 h4 = head 4 h5 = head 5 h6 = head 6 unorder_list = disorder list order_list = order list hline = horizontal line link = link ref_link = reference link add_pic = add picture code = code code_block = code block table = add table quote = quote gfm_task = GFM task attachment = attachment json_to_table = Json converted to table template = template draw = draw close_preview = disable preview modify_history = modify history sidebar = sidebar help = help publish = publish document = document create_doc = create document attachments = attachments doc_name = Document name doc_name_tips = Right-click on the document name of the directory to delete and modify the document name and add subordinate documents doc_id = Document ID doc_id_tips = The document ID can only contain lowercase letters, numbers, and "-" and "_" symbols, and can only start with a lowercase letter expand_desc = (Expand nodes when reading) fold_desc = (Fold nodes when reading) empty_contents = Empty contents empty_contents_desc = Click to expand the subordinate nodes upload_attachment = Upload attachment doc_history = Document history choose_template_type = Choose template type please normal_tpl = Normal api_tpl = API data_dict = Data Dictionary custom_tpl = Custom tpl_default_type = Default type tpl_plain_text = Plain Text for_api_doc = Used for API Document code_highlight = support code highlighting for_data_dict = Used for data dictionary form_support = Form Support any_type_doc = Support any type of document as_global_tpl = Can be set as a global template tpl_name = Template name tpl_type = Template type creator = Creator create_time = Create time operation = Operation global_tpl = Global global_tpl_desc = (Any Project is available) project_tpl = Project project_tpl_desc = (Only the current Project is available) insert = insert uploading = Uploading his_ver = Historic version update_time = Update time updater = Updater version = Version delete = Delete recover = Recover merge = Merge comparison_title = Document comparison [the left is the historical document, the right is the current document, please merge the documents to the right] font_size = font size underscore = underscore right_intent = right intent left_intent = left intent subscript = subscript superscript = superscript clean_format = clear format add_video = add video formula = formula font_color = font color bg_color = background color input_pwd = input password please read_pwd = read password commit = commit ft_author = Author: ft_last_editor = Last editor: ft_create_time = Create time: ft_update_time = Update time: view_count = Number of views changetheme = Switch themes prev = prev next = next [project] prj_space_list = Project Space List prj_space_list_of = List of Project Space %s search_title = Show Items of Project Space "%s" author = Author no_project = No Project prj_amount = Project Count creator = Creator create_time = Create time no_project_space = No Project Space [search] title = Search search_title = Show search result for %s doc = document prj = project blog = blog from_proj = from project from_blog = from blog author = author update_time = update time no_result = No search result [page] first = first last = last prev = prev next = next [uc] user_center = User Center base_info = Basic Info change_pwd = Change Password username = Username nickname = Nickname realname = Real name email = Email mobile = Mobile description = Description description_tips = Description cannot exceed 500 characters avatar = Avatar change_avatar = Change avatar password = Password origin_pwd = Origin password new_pwd = New password confirm_pwd = Confirm password role = Role type = Type status = Status super_admin = Super administrator admin = Administrator user = User read_usr = Read-Only User normal = Normal disable = Disable enable = Enable create_user = Create User edit_user = Edit User pwd_tips = Please leave it blank if you do not change the password, only local users can change the password [mgr] language = Default Language zh_cn = 简体中文 en_us = English dashboard_menu = Dashboard user_menu = User team_menu = Team project_menu = Project project_space_menu = Project Space comment_menu = Comment config_menu = Configure attachment_menu = Attachment label_menu = Label dashboard_mgr = Dashboard user_mgr = User Management team_mgr = Team Management project_mgr = Project Management project_space_mgr = Project Space Management comment_mgr = Comment Management config_mgr = Configure Management config_file = Configure File attachment_mgr = Attachment Management label_mgr = Label Management label_name = Label name used_quantity = Used Quantity proj_amount = Number Of Project blog_amount = Number Of Blog member_amount = Number Of Member comment_amount = Number Of Comment attachment_amount = Number Of Attachment member_mgr = Member Management add_member = Add Member create_team = Create Team team_name = Team Name proj = Project member = Member edit_team = Edit Team team_member_mgr = Team Member Management team_proj = Team Project add_proj = Add Project proj_name = Project name proj_author = Project author join_time = Join Time join_proj = Join file_name = File name is_exist = Is Exist exist = Exist deleted = Deleted proj_blog_name = Project/Blog name doc_name = Document name file_path = File path download_url = Download URL file_size = File size upload_time = Upload time download = Download download_title = Download to local attachment_name = Attachment name site_name = Site Name domain_icp = Domain ICP site_desc = Site Description site_desc_tips = Description cannot exceed 500 characters enable_anonymous_access = Enable anonymous access enable = Enable disable = Disable enable_register = Enable Registration enable_captcha = Enable Captcha enable_doc_his = Enable Document Historic proj_space_name = Project space name proj_space_id = Project space ID create_proj_space = Create Project Space edit_proj_space = Edit Project Space proj_list = Project List edit_proj = Edit Project create_time = Create Time creator = Creator doc_amount = Number of Document last_edit = Last Edit delete_project = Delete Project ================================================ FILE: conf/lang/ru-ru.ini ================================================ [common] title = MinDoc home = Домашняя blog = Блог project_space = Проекты person_center = Профиль my_project = Мои проекты my_blog = Моя статья manage = Управление login = Войти logout = Выйти official_website = Официальный сайт feedback = Обратная связь source_code = Исходный код manual = Руководство пользователя username = Имя пользователя account = Аккаунт email = Email password = Пароль role = Роль captcha = Капча keep_login = Оставайтесь в системе forgot_password = забыл пароль? register = Создать новый аккаунт third_party_login = Вход третьей стороны dingtalk_login = Войти с помощью DingTalk wecom_login = Войти через Wecom account_recovery = Восстановление аккаунта new_password = Новый пароль confirm_password = Подтвердите пароль new_account = Создать новую учетную запись setting = Настройка save = Сохранить edit = Изменить delete = Удалить cancel = Отмена create = Создать confirm_delete = Подтверждать upload_lang = ru js_lang = ru-RU remove = Удалять operate = Оперировать confirm = Подтверждать creator = Создатель administrator = Администратор editor = Редактор observer = Наблюдатель back = Назад detail = Деталь admin_right = Чтение, письмо и управление editor_right = Чтение и письмо observer_right = Только чтение yes = Да no = Нет read = Читать generate = Генерировать clean = Чистый [init] default_proj_name = Демонстрационный проект MinDoc default_proj_desc = Это демонстрационный проект MinDoc, который автоматически создается при инициализации системы default_proj_space = Пространство проекта по умолчанию blank_doc = Пустой документ [message] tips = Советы page_not_existed = Эта страница не существует no_permission = доступ запрещен keyword_placeholder = введите ключевое слово, пожалуйста... wrong_account_password = Неправильное имя пользователя или пароль wrong_password = Неправильный пароль click_to_change = Нажмите, чтобы изменить один logging_in = Вход в систему... need_relogin = Пожалуйста, войдите снова return_account_login = вернуться к входу с учетной записью и паролем no_account_yet = У вас еще нет аккаунта? has_account = У вас уже есть аккаунт? account_empty = Счет не может быть пустым email_empty = Электронная почта не может быть пустой password_empty = Пароль не может быть пустым captcha_empty = Капча не может быть пустой system_error = Система обнаружила ошибку processing = Обработка... email_sent = Письмо успешно отправлено, пожалуйста, проверьте его confirm_password_empty = Подтвержденный пароль не может быть пустым incorrect_confirm_password = Неверный подтвержденный пароль illegal_request = Незаконный запрос account_or_password_empty = Учетная запись или пароль не могут быть пустыми captcha_wrong = Неправильная капча password_length_invalid = Пароль не может быть пустым и должен содержать от 6 до 50 символов mail_expired = почта просрочена captcha_expired = Срок действия капчи истек, попробуйте еще раз user_not_existed = этот пользователь не существует readusr_only_observer = Толькі для чытання карыстальнікаў можна ўсталяваць толькі як назіральнікі email_not_exist = этот адрес электронной почты не существует failed_save_password = Не удалось сохранить пароль mail_service_not_enable = Служба электронной почты не включена account_disable = эта учетная запись была отключена failed_send_mail = Не удалось отправить электронное письмо sent_too_many_times = Отправьте слишком много раз, попробуйте еще раз позже account_not_support_retrieval = Текущий пользователь не поддерживает восстановление пароля username_invalid_format = Имя учетной записи может состоять только из английских букв, цифр и 3–50 символов email_invalid_format = Неверный формат электронной почты account_existed = Имя пользователя уже существует failed_register = Регистрация не удалась, обратитесь к системному администратору failed_obtain_user_info = Не удалось получить идентификационную информацию dingtalk_auto_login_not_enable = Функция автоматического входа в DingTalk не включена failed_auto_login = Автоматический вход не удался no_project = Нет проекта item_not_exist = Элемент не существует или был удален item_not_exist_or_no_permit = Элемент не существует или имеет недостаточно прав doc_not_exist = Документ не существует или был удален doc_not_exist_or_no_permit = Документ не существует или имеет недостаточно прав unknown_exception = Неизвестное исключение no_data = Нет данных project_must_belong_space = Каждый проект должен принадлежать к проектному пространству, и его администратор может им управлять project_title_placeholder = Название (ограничение в 30 слов) project_title_tips = Название проекта не может превышать 100 символов project_id_placeholder = Идентификатор проекта (ограничение в 30 символов) project_id_tips = Идентификатор может содержать только строчные буквы, цифры и символы «-», «.» и «_» project_desc_placeholder = Описание проекта не может превышать 500 символов project_public_desc = (Доступ возможен для всех) project_private_desc = (Доступ возможен только участникам или пользователям токенов) project_cover_desc = Изображение обложки проекта можно изменить в настройках проекта confirm_delete_project = Вы уверены, что хотите удалить проект? warning_delete_project = После удаления проекта его невозможно восстановить project_space_empty = Пожалуйста, выберите проектное пространство project_title_empty = Название проекта не может быть пустым project_id_empty = ID проекта не может быть пустым project_id_existed = Идентификатор проекта уже используется project_id_error = Неверный идентификатор проекта project_id_length = Идентификатор проекта должен быть менее 50 символов import_file_empty = Пожалуйста, выберите файл для загрузки file_type_placeholder = Пожалуйста, выберите файл zip/docx publish_to_queue = Задача публикации помещена в очередь задач и будет выполнена в ближайшее время team_name_empty = Название команды не может быть пустым operate_failed = Операция не удалась project_id_desc = Идентификатор проекта используется для обозначения уникальности элемента и не может быть изменен history_record_amount_desc = Если включена история документа, это значение ограничивает количество сохраненных историй для каждого документа corp_id_desc = Нижний колонтитул, который появляется при экспорте документа PDF project_desc_desc = Описание информации не более 500 символов, поддерживает синтаксис markdown project_desc_tips = Описательная информация не более 500 символов access_pass_desc = Пароль, который вам необходимо указать, если у вас нет разрешения на доступ к проекту auto_publish_desc = После включения каждое сохранение будет автоматически публиковаться в последней версии enable_export_desc = Перед включением экспорта настройте программу экспорта и одновременно включите функцию экспорта в файле конфигурации enable_share_desc = Совместное использование доступно только для публичных проектов. Частные проекты не поддерживают совместное использование auto_save_desc = Сохранять автоматически каждые 30 секунд confirm_into_private = Вы уверены, что хотите сделать проект приватным? into_private_notice = Частный проект должен предоставить токен доступа confirm_into_public = Вы уверены, что хотите сделать свой проект публичным? into_public_notice = Публичный проект может посетить любой желающий project_name_empty = Название проекта не может быть пустым success = Успех failed = Неуспешный receive_account_empty = Счет получателя не может быть пустым receive_account_not_exist = Учетная запись получателя не существует receive_account_disabled = Отключить учетную запись получателя cannot_preview = Невозможно просмотреть upload_failed = Загрузка не удалась upload_file_size_limit = Файл должен быть меньше 2MB upload_file_empty = Загруженный файл пуст uploda_file_type_error = Неправильный тип загруженного файла choose_pic_file = Пожалуйста, выберите изображение no_doc_in_cur_proj = Нет документов по текущему проекту build_doc_tree_error = Произошла ошибка при построении дерева документов проекта param_error = Ошибка параметра doc_name_empty = Имя документа не может быть пустым parent_id_not_existed = Родительский идентификатор не существует doc_not_belong_project = Документ не принадлежит указанному проекту attachment_not_exist = Вложение не существует read_file_error = Ошибка чтения документа confirm_override_doc = Документ был изменен. Вы уверены, что хотите его отменить? doc_auto_published = Документ был автоматически успешно опубликован export_func_disable = Функция экспорта отключена cur_project_export_func_disable = Функция экспорта отключена для текущего проекта cur_project_not_support_md = Редактор Markdown не поддерживается для текущего проекта export_failed = Экспорт не удался, проверьте системный журнал file_converting = Документ конвертируется в фоновом режиме, пожалуйста, загрузите его позже unsupport_file_type = Неподдерживаемый тип файла no_exportable_file = Проект не имеет экспортируемого файла gen_qrcode_failed = Сгенерировать QR-код не удалось search_result_error = Ошибка поиска get_doc_his_failed = Не удалось получить историю документа project_space_not_exist = Проектное пространство не существует search_placeholder = введите ключевое слово пожалуйста no_search_result = Результаты поиска отсутствуют! user_exist_in_proj = Пользователь уже существует в проекте cannot_change_own_priv = Невозможно изменить собственные разрешения cannot_delete_self = Невозможно удалить себя cannot_handover_myself = Cannot be transferred to yourself confirm_delete_blog = Подтвердить удаление блога? delete_blog_tips = Удаленный блог не может быть восстановлен input_proj_id_pls = введите идентификатор проекта, пожалуйста input_doc_id_pls = введите идентификатор документа, пожалуйста blog_digest_tips = Дайджест блога не может превышать 500 символов blog_title_empty = название блога не может быть пустым blog_not_exist = Блог не существует или был удален blog_pwd_incorrect = Неверный пароль доступа set_pwd_pls = пожалуйста, установите пароль для шифрования блога unknown_blog_type = неизвестный тип блога blog_title_tips = Название блога не может превышать 200 символов ref_doc_prj_not_existed = Проект справочного документа не существует query_ref_doc_error = запрос справочного документа не удался ref_doc_not_exist_or_no_permit = справочный документ не существует или имеет недостаточные разрешения blog_id_existed = идентификатор блога уже существует query_failed = запрос не удался blog_has_modified = Статья была изменена cur_user_cannot_change_pwd = Текущий пользователь не поддерживает смену пароля origin_pwd_empty = Исходный пароль не может быть пустым new_pwd_empty = Новый пароль не может быть пустым confirm_pwd_empty = Подтверждение пароля не может быть пустым pwd_length = Пароль должен содержать от 6 до 18 символов pwd_length_tips = Пароль должен содержать от 6 до 50 символов wrong_origin_pwd = Неверный пароль источника wrong_confirm_pwd = Подтверждение пароля неверное same_pwd = Новый пароль должен отличаться от исходного pwd_encrypt_failed = Шифрование пароля не удалось team_name_empty = Название команды не может быть пустым proj_empty = Проект не может быть пустым site_name_empty = Имя сайта не может быть пустым proj_space_name_empty = Имя пространства проекта не может быть пустым proj_space_id_empty = Идентификатор пространства проекта не может быть пустым proj_space_id_tips = Идентификатор пространства проекта может состоять только из букв и цифр и содержать от 2 до 100 символов project_order_desc = Только число, сортировать от большего к меньшему project_label_desc = Максимально допустимое количество тегов — 10. Разделяйте несколько тегов знаком «;» cannot_change_own_status = Невозможно изменить свой статус cannot_change_super_status = Невозможно изменить статус суперадминистратора cannot_change_super_priv = Невозможно изменить права суперадминистратора editors_not_compatible = Эти два редактора несовместимы [blog] author = Автор project_list = Список проектов add_project = Добавить проект import_project = Импорт проекта delete_project = Удалить проект project_summary = Резюме проекта read = Читать edit = Редактировать delete = Удалить copy = Копировать view = Вид publish = Публиковать edit_doc = Редактировать документ default_cover = Обложка по умолчанию create_time = Время создания update_time = Время обновления creator = Создатель doc_amount = Количество документов doc_unit = project_role = Роль проекта last_edit = последний раз редактировалось project_title = Название проекта project_id = Идентификатор проекта project_desc = Описание проекта public = общественный private = Частный public_project = Общественный проект private_project = Частный проект summary = Краткое содержание member = Член team = Команда comment_amount = Количество комментариев comment_unit = member_manage = управление членами add_member = Добавить участника administrator = Администратор editor = Редактор observer = Наблюдатель team_manage = управление командой add_team = Добавить команду team_name = Название команды member_amount = Количество членов join_time = Присоединился Время project_setting = Настройка проекта handover_project = Проект передачи make_public = В общественность make_private = В частную жизнь history_record_amount = Количество исторических записей corp_id = название корпорации text_editor = монтажер project_label = Метка проекта project_order = Сортировка проекта access_pass = Пароль доступа access_token = Токен доступа auto_publish = Автоматическая публикация enable_export = Включить экспорт enable_share = Включить Поделиться set_first_as_home = Установить первый документ в качестве домашней страницы по умолчанию auto_save = Автоматическое сохранение cover = крышка click_change_cover = Нажмите, чтобы изменить обложку проекта change_cover = сменить обложку preview = Предварительный просмотр choose = Выбирать upload = Загрузить recipient_account = Получатель blog_list = Список блогов add_blog = Добавить блог encryption = шифрование encrypt = шифровать edit_blog = Редактировать блог delete_blog = Удалить блог setting_blog = Настройка блога no_blog = Нет блога blog_setting = Настройка блога title = Название блога type = Тип блога normal_blog = Обычный блог link_blog = Связанный блог ref_doc = Справочный документ blog_status = Статус блога blog_pwd = Пароль блога blog_digest = Блог Дайджест posted_on = Опубликовано modified_on = Изменено prev = предыдущий next = следующий no = нет edit_title = Редактировать блог private_blog_tips = Это частный блог, введите пароль для доступа print_text = Включить печать [doc] word_to_html = Word в HTML html_to_markdown = HTML в Markdown modify_doc = Изменить документ comparison = Сравнение save_merge = Сохранить Объединить prev_diff = Предыдущая разница next_diff = Следующее отличие merge_to_left = Объединить слева merge_to_right = Объединить справа exchange_left_right = Обмен левого и правого print = Печать download = Скачать share = Делиться share_project = Поделиться проектом share_url = URL-адрес проекта contents = Каталог search = Поиск expand = Расширять fold = Складывать close = Закрывать doc_publish_by = Документ опубликован doc_publish = выпускать edit_doc = Редактировать документ backward = назад save = Сохранять save_as_tpl = Сохранить как шаблон undo = Отменить redo = Переделать bold = жирный italic = курсив strikethrough = зачеркивание h1 = H1 h2 = H2 h3 = H3 h4 = H4 h5 = H5 h6 = H6 unorder_list = Неупорядоченный список order_list = Упорядоченный список hline = Горизонтальная линия link = Связь ref_link = Ссылка на ссылку add_pic = добавить картинку code = код code_block = блок кода table = Добавить таблицу quote = цитировать gfm_task = Задача GFM attachment = вложение json_to_table = конвертировать json в таблицу template = шаблон draw = нарисовать close_preview = отключить предварительный просмотр modify_history = История модификаций sidebar = боковая панель help = помощь publish = публиковать document = документ create_doc = создать документ attachments = вложения doc_name = Название документа doc_name_tips = Щелкните правой кнопкой мыши по имени документа в каталоге, чтобы удалить и изменить имя документа, а также добавить подчиненные документы. doc_id = Идентификатор документа doc_id_tips = Идентификатор документа может содержать только строчные буквы, цифры, символы «-» и «_» и может начинаться только со строчной буквы. expand_desc = (Расширять узлы при чтении) fold_desc = (Сворачивайте узлы при чтении) empty_contents = Пустой каталог empty_contents_desc = Нажмите, чтобы развернуть подчиненные узлы upload_attachment = Загрузить вложение doc_history = История документа choose_template_type = Выберите тип шаблона, пожалуйста normal_tpl = Нормальный api_tpl = API data_dict = Словарь данных custom_tpl = Обычай tpl_default_type = Тип по умолчанию tpl_plain_text = Обычный текст for_api_doc = Используется для API-документа code_highlight = поддержка подсветки кода for_data_dict = Используется для словаря данных form_support = Поддержка формы any_type_doc = Поддержка любого типа документа as_global_tpl = Можно установить как глобальный шаблон tpl_name = Имя шаблона tpl_type = Тип шаблона creator = Создатель create_time = Время создания operation = Операция global_tpl = Глобальный global_tpl_desc = (Любой проект доступен) project_tpl = Проект project_tpl_desc = (Доступен только текущий проект) insert = вставлять uploading = Загрузка his_ver = Историческая версия update_time = Время обновления updater = Обновление version = Версия delete = Удалить recover = Восстанавливаться merge = Слияние comparison_title = Сравнение документов [слева — исторический документ, справа — текущий документ, пожалуйста, объедините документы справа] font_size = размер шрифта underscore = подчеркивание right_intent = правильное намерение left_intent = левое намерение subscript = нижний индекс superscript = верхний индекс clean_format = очистить формат add_video = добавить видео formula = формула font_color = цвет шрифта bg_color = цвет фона input_pwd = введите пароль пожалуйста read_pwd = прочитать пароль commit = совершить ft_author = Автор: ft_last_editor = Последний редактор: ft_create_time = Время создания: ft_update_time = Время обновления: view_count = Количество просмотров changetheme = Переключить темы [project] prj_space_list = Список проектных пространств prj_space_list_of = Список проектного пространства %s search_title = Показать элементы пространства проекта "%s" author = Автор no_project = Нет проекта prj_amount = Количество проектов creator = Создатель create_time = Время создания no_project_space = Нет места для проекта [search] title = Поиск search_title = Показать результат поиска для "%s" doc = документ prj = проект blog = блог from_proj = из проекта from_blog = из блога author = автор update_time = время обновления no_result = Нет результатов поиска [page] first = первый last = последний prev = предыдущий next = следующий [uc] user_center = Центр пользователя base_info = основная информация change_pwd = Изменить пароль username = Имя пользователя nickname = Псевдоним realname = Настоящее имя email = Email mobile = Номер телефона description = Описание description_tips = Описание не может превышать 500 символов. avatar = Аватар change_avatar = Изменить аватар password = Пароль origin_pwd = Исходный пароль new_pwd = Новый пароль confirm_pwd = Подтвердите пароль role = Роль type = Тип status = Статус super_admin = Супер администратор admin = Администратор user = Пользователь read_usr = Пользователи только для чтения normal = Нормальный disable = Отключено enable = Включено create_user = Добавить пользователя edit_user = Редактировать пользователя pwd_tips = Пожалуйста, оставьте поле пустым, если вы не меняете пароль. Изменить пароль могут только локальные пользователи. [mgr] language = Язык по умолчанию zh_cn = 简体中文 en_us = English dashboard_menu = Приборная панель user_menu = Пользователи team_menu = Команды project_menu = Проекты project_space_menu = Проектные пространства comment_menu = Комментарии config_menu = Конфигурирует attachment_menu = Вложения label_menu = Этикетки dashboard_mgr = Приборная панель user_mgr = Управление пользователями team_mgr = Управление командой project_mgr = Управление проектом project_space_mgr = Управление пространством проекта comment_mgr = Управление комментариями config_mgr = Конфигурирует управление config_file = Настроить файл attachment_mgr = Управление вложениями label_mgr = Управление этикетками label_name = Название этикетки used_quantity = Использованное количество proj_amount = Количество проектов blog_amount = Количество блогов member_amount = Количество членов comment_amount = Количество комментариев attachment_amount = Количество вложений member_mgr = Управление участниками add_member = Добавить участника create_team = Создать команду team_name = Название команды proj = Проект member = Член edit_team = Редактировать информацию о команде team_member_mgr = Управление членами команды team_proj = Командный проект add_proj = Добавить проект proj_name = Название проекта proj_author = Автор проекта join_time = Присоединяйтесь Время join_proj = Присоединиться file_name = Имя файла is_exist = Существует exist = Существовать deleted = Удалено proj_blog_name = Название проекта/блога doc_name = Название документа file_path = Путь файла download_url = URL для загрузки файла file_size = Размер файла upload_time = Время, когда файл был загружен download = Скачать download_title = Загрузить на локальный путь attachment_name = Имя вложения site_name = Название сайта domain_icp = Регистрационный номер доменного имени ICP site_desc = Описание веб-сайта site_desc_tips = Описание не может превышать 500 символов enable_anonymous_access = Включить анонимный доступ enable = Включить disable = Отключить enable_register = Включить регистрацию enable_captcha = Включить капчу enable_doc_his = Включить историю документов proj_space_name = Название пространства проекта proj_space_id = Идентификатор пространства проекта create_proj_space = Создать пространство проекта edit_proj_space = Редактировать пространство проекта proj_list = Список проектов edit_proj = Редактировать проект create_time = Время создания creator = Создатель doc_amount = Количество документов last_edit = Последний редактор delete_project = Удалить проект ================================================ FILE: conf/lang/zh-cn.ini ================================================ [common] title = 文档在线管理系统 home = 首页 blog = 文章 project_space = 项目空间 person_center = 个人中心 my_project = 我的项目 my_blog = 我的文章 manage = 管理后台 login = 登录 logout = 退出登录 official_website = 官方网站 feedback = 意见反馈 source_code = 项目源码 manual = 使用手册 username = 用户名 account = 账号 email = 邮箱 password = 密码 role = 角色 captcha = 验证码 keep_login = 保持登录 forgot_password = 忘记密码? register = 立即注册 third_party_login = 第三方登录 dingtalk_login = 钉钉登录 wecom_login = 企业微信登录 account_recovery = 找回密码 new_password = 新密码 confirm_password = 确认密码 new_account = 用户注册 setting = 设置 save = 保存 edit = 编辑 delete = 删除 cancel = 取消 create = 创建 confirm_delete = 确定删除 upload_lang = zh js_lang = zh-CN remove = 移除 operate = 操作 confirm = 确定 creator = 创始人 administrator = 管理员 editor = 编辑者 observer = 观察者 back = 返回 detail = 详情 admin_right = 拥有阅读、写作和管理权限 editor_right = 拥有阅读和写作权限 observer_right = 拥有阅读权限 yes = 是 no = 否 read = 阅读 generate = 生成 clean = 清理 [init] default_proj_name = MinDoc演示项目 default_proj_desc = 这是一个MinDoc演示项目,该项目是由系统初始化时自动创建。 default_proj_space = 默认项目空间 blank_doc = 空白文档 [message] tips = 友情提示 page_not_existed = 页面不存在 no_permission = 权限不足 keyword_placeholder = 请输入关键词... wrong_account_password = 账号或密码错误 wrong_password = 密码错误 click_to_change = 点击换一张 logging_in = 正在登录... need_relogin = 请重新登录。 return_account_login = 返回账号密码登录 no_account_yet = 还没有账号? has_account = 已有账号? account_empty = 账号不能为空 email_empty = 邮箱不能为空 password_empty = 密码不能为空 captcha_empty = 验证码不能为空 system_error = 系统错误 processing = 正在处理... email_sent = 邮件发送成功,请登录邮箱查看。 confirm_password_empty = 确认密码不能为空 incorrect_confirm_password = 确认密码输入不正确 illegal_request = 非法请求 account_or_password_empty = 账号或密码不能为空 captcha_wrong = 验证码不正确 password_length_invalid = 密码不能为空且必须在6-50个字符之间 mail_expired = 邮件已失效 captcha_expired = 验证码已过期,请重新操作。 user_not_existed = 用户不存在 readusr_only_observer = 只读用户只能设置为观察者 email_not_exist = 邮箱不存在 failed_save_password = 保存密码失败 mail_service_not_enable = 未启用邮件服务 account_disable = 账号已被禁用 failed_send_mail = 发送邮件失败 sent_too_many_times = 发送次数太多,请稍候再试 account_not_support_retrieval = 当前用户不支持找回密码 username_invalid_format = 账号只能由英文字母数字组成,且在3-50个字符 email_invalid_format = 邮箱格式不正确 account_existed = 账号已存在 failed_register = 注册失败,请联系管理员 failed_obtain_user_info = 获取身份信息失败 dingtalk_auto_login_not_enable = 未开启钉钉自动登录功能 failed_auto_login = 自动登录失败 no_project = 暂无项目 item_not_exist = 项目不存在或已删除 item_not_exist_or_no_permit = 项目不存在或权限不足 doc_not_exist = 文档不存在或已删除 doc_not_exist_or_no_permit = 文档不存在或权限不足 unknown_exception = 未知异常 no_data = 暂无数据 project_must_belong_space = 每个项目必须归属一个项目空间,超级管理员可在后台管理和维护 project_title_placeholder = 项目标题(不超过100字) project_title_tips = 项目标题不能超过100字符 project_id_placeholder = 项目唯一标识(不超过50字) project_id_tips = 文档标识只能包含小写字母、数字,以及“-”、“.”和“_”符号. project_desc_placeholder = 描述信息不超过500个字符 project_public_desc = (任何人都可以访问) project_private_desc = (只有参与者或使用令牌才能访问) project_cover_desc = 项目图片可在项目设置中修改 confirm_delete_project = 确定删除项目吗? warning_delete_project = 删除项目后将无法找回。 project_space_empty = 请选择项目空间 project_title_empty = 项目标题不能为空 project_id_empty = 项目标识不能为空 project_id_existed = 文档标识已被使用 project_id_error = 项目标识有误 project_id_length = 项目标识必须小于50字符 import_file_empty = 请选择需要上传的文件 file_type_placeholder = 请选择Zip或Docx文件 publish_to_queue = 发布任务已推送到任务队列,稍后将在后台执行。 team_name_empty = 团队名称不能为空 operate_failed = 操作失败 project_id_desc = 项目标识用来标记项目的唯一性,不可修改。 history_record_amount_desc = 当开启文档历史时,该值会限制每个文档保存的历史数量 corp_id_desc = 导出文档PDF文档时显示的页脚 project_desc_desc = 描述信息不超过500个字符,支持Markdown语法 project_desc_tips = 项目描述不能大于500字 access_pass_desc = 没有访问权限访问项目时需要提供的密码 auto_publish_desc = 开启后,每次保存会自动发布到最新版本 enable_export_desc = 开启导出前请先配置导出程序,并在配置文件中同时开启导出功能 enable_share_desc = 分享只对公开项目生效,私有项目不支持分享 auto_save_desc = 开启后每隔30秒会自动保存 confirm_into_private = 确定将项目转为私有吗? into_private_notice = 转为私有后需要通过阅读令牌才能访问该项目。 confirm_into_public = 确定将项目转为公有吗? into_public_notice = 转为公有后所有人都可以访问该项目。 project_name_empty = 项目名称不能为空 success = 成功 failed = 失败 receive_account_empty = 接受者账号不能为空 receive_account_not_exist = 接受用户不存在 receive_account_disabled = 接受用户已被禁用 cannot_preview = 不能预览 upload_failed = 上传失败 upload_file_size_limit = 文件必须小于2MB upload_file_empty = 上传文件为空 uploda_file_type_error = 文件类型有误 choose_pic_file = 请选择图片 no_doc_in_cur_proj = 当前项目没有文档 build_doc_tree_error = 生成项目文档树时出错 param_error = 参数错误 doc_name_empty = 文档名称不能为空 parent_id_not_existed = 父分类不存在 doc_not_belong_project = 文档不属于指定的项目 attachment_not_exist = 附件不存在或已删除 read_file_error = 读取文档错误 confirm_override_doc = 文档已被修改确定要覆盖吗? doc_auto_published = 文档自动发布成功 export_func_disable = 系统没有开启导出功能 cur_project_export_func_disable = 当前项目没有开启导出功能 cur_project_not_support_md = 当前项目不支持Markdown编辑器 export_failed = 导出失败,请查看系统日志 file_converting = 文档正在后台转换,请稍后再下载 unsupport_file_type = 不支持的文件格式 no_exportable_file = 项目没有导出文件 gen_qrcode_failed = 生成二维码失败 search_result_error = 搜索结果错误 get_doc_his_failed = 获取历史失败 project_space_not_exist = 项目空间不存在 search_placeholder = 请输入搜索关键字 no_search_result = 暂无相关搜索结果! user_exist_in_proj = 用户已存在该项目中 cannot_change_own_priv = 不能变更自己的权限 cannot_delete_self = 不能删除自己 cannot_handover_myself = 不能转让给自己 confirm_delete_blog = 确定删除文章吗? delete_blog_tips = 删除文章后将无法找回。 input_proj_id_pls = 请输入项目标识 input_doc_id_pls = 请输入文档标识 blog_digest_tips = 文章摘要不超过500个字符 blog_title_empty = 文章标题不能为空 blog_not_exist = 文章不存在或已删除 blog_pwd_incorrect = 文章密码不正确 set_pwd_pls = 加密文章请设置密码 unknown_blog_type = 未知的文章类型 blog_title_tips = 文章标题不能大于200个字符 ref_doc_prj_not_existed = 关联文档的项目不存在 query_ref_doc_error = 查询关联项目文档时出错 ref_doc_not_exist_or_no_permit = 关联文档不存在或权限不足 blog_id_existed = 文章标识已存在 query_failed = 查询失败 blog_has_modified = 文章已被修改 cur_user_cannot_change_pwd = 当前用户不支持修改密码 origin_pwd_empty = 原密码不能为空 new_pwd_empty = 新密码不能为空 confirm_pwd_empty = 确认密码不能为空 pwd_length = 密码必须在6-18字之间 pwd_length_tips = 密码必须在6-50个字符之间 wrong_origin_pwd = 原始密码不正确 wrong_confirm_pwd = 确认密码不正确 same_pwd = 新密码不能和原始密码相同 pwd_encrypt_failed = 密码加密失败 team_name_empty = 团队名称不能为空 proj_empty = 项目不能为空 site_name_empty = 网站标题不能为空 proj_space_name_empty = 项目空间名称不能为空 proj_space_id_empty = 项目空间标识不能为空 proj_space_id_tips = 项目空间标识只能由字母和数字组成且在2-100字符之间 project_order_desc = 只能是数字,序号越大排序越靠前 project_label_desc = 最多允许添加10个标签,多个标签请用“;”分割 cannot_change_own_status = 不能变更自己的状态 cannot_change_super_status = 不能变更超级管理员的状态 cannot_change_super_priv = 不能变更超级管理员的权限 editors_not_compatible = 两种编辑器不兼容 [blog] author = 作者 project_list = 项目列表 add_project = 添加项目 import_project = 导入项目 delete_project = 删除项目 project_summary = 项目概要 read = 阅读 edit = 编辑 delete = 删除 copy = 复制 view = 查看文档 publish = 发布 edit_doc = 编辑文档 default_cover = 默认封面 create_time = 创建时间 update_time = 修改时间 creator = 创建者 doc_amount = 文档数量 doc_unit = 篇 project_role = 项目角色 last_edit = 最后编辑 project_title = 项目标题 project_id = 项目标识 project_desc = 项目描述 public = 公开 private = 私有 public_project = 公开项目 private_project = 私有项目 summary = 概要 member = 成员 team = 团队 comment_amount = 评论数量 comment_unit = 条 member_manage = 成员管理 add_member = 添加成员 administrator = 管理员 editor = 编辑者 observer = 观察者 team_manage = 团队管理 add_team = 添加团队 team_name = 团队名称 member_amount = 成员数量 join_time = 加入时间 project_setting = 项目设置 handover_project = 转让项目 make_public = 转为公有 make_private = 转为私有 history_record_amount = 历史记录数量 corp_id = 公司名称 text_editor = 编辑器 project_label = 项目标签 project_order = 项目排序 access_pass = 访问密码 access_token = 访问令牌 auto_publish = 自动发布 enable_export = 开启导出 enable_share = 开启分享 set_first_as_home = 设置第一篇文档为默认首页 auto_save = 自动保存 cover = 封面 click_change_cover = 点击图片可修改项目封面 change_cover = 修改封面 preview = 预览 choose = 选择 upload = 上传 recipient_account = 接收者账号 blog_list = 文章列表 add_blog = 添加文章 encryption = 加密 encrypt = 密 edit_blog = 文章编辑 delete_blog = 删除文章 setting_blog = 文章设置 no_blog = 暂无文章 blog_setting = 文章设置 title = 文章标题 type = 文章类型 normal_blog = 普通文章 link_blog = 链接文章 ref_doc = 关联文档 blog_status = 文章状态 blog_pwd = 文章密码 blog_digest = 文章摘要 posted_on = 发布于 modified_on = 修改于 prev = 上一篇 next = 下一篇 no = 无 edit_title = 编辑文章 private_blog_tips = 加密文章,请输入密码访问 print_text = 开启打印 [doc] word_to_html = Word转笔记 html_to_markdown = HTML转Markdown modify_doc = 修改文档 comparison = 文档比较 save_merge = 保存合并 prev_diff = 上一处差异 next_diff = 下一处差异 merge_to_left = 合并到左侧 merge_to_right = 合并到右侧 exchange_left_right = 左右切换 print = 打印 download = 下载 share = 分享 share_project = 项目分享 share_url = 项目地址 contents = 目录 search = 搜索 expand = 展开 fold = 收起 close = 关闭 doc_publish_by = 本文档使用 doc_publish = 发布 edit_doc = 编辑文档 backward = 返回 save = 保存 save_as_tpl = 保存为模板 undo = 撤销 redo = 重做 bold = 粗体 italic = 斜体 strikethrough = 删除线 h1 = 标题一 h2 = 标题二 h3 = 标题三 h4 = 标题四 h5 = 标题五 h6 = 标题六 unorder_list = 无序列表 order_list = 有序列表 hline = Horizontal line link = 链接 ref_link = 引用链接 add_pic = 添加图片 code = 行内代码 code_block = 代码块 table = 添加表格 quote = 引用 gfm_task = GFM 任务列表 attachment = 附件 json_to_table = Json转换为表格 template = 模板 draw = 画图 close_preview = 关闭实时预览 modify_history = 修改历史 sidebar = 边栏 help = 使用帮助 publish = 发布 document = 文档 create_doc = 创建文档 attachments = 个附件 doc_name = 文档名称 doc_name_tips = 在目录的文档名上右键可以删除和修改文档名称以及添加下级文档 doc_id = 文档标识 doc_id_tips = 文档标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头 expand_desc = (在阅读时会自动展开节点) fold_desc = (在阅读时会关闭节点) empty_contents = 空目录 empty_contents_desc = (单击时会展开下级节点) upload_attachment = 上传附件 doc_history = 文档历史记录 choose_template_type = 请选择模板类型 normal_tpl = 普通文档 api_tpl = API文档 data_dict = 数据字典 custom_tpl = 自定义模板 tpl_default_type = 默认类型 tpl_plain_text = 简单的文本文档 for_api_doc = 用于API文档速写 code_highlight = 支持代码高亮 for_data_dict = 用于数据字典显示 form_support = 表格支持 any_type_doc = 支持任意类型文档 as_global_tpl = 可以设置为全局模板 tpl_name = 模板名称 tpl_type = 模板类型 creator = 创建人 create_time = 创建时间 operation = 操作 global_tpl = 全局 global_tpl_desc = (任何项目都可用) project_tpl = 项目 project_tpl_desc = (只有当前项目可用) insert = 插入 uploading = 正在上传 his_ver = 历史版本 update_time = 修改时间 updater = 修改人 version = 版本 delete = 删除 recover = 恢复 merge = 合并 comparison_title = 文档比较【左侧为历史文档,右侧为当前文档,请将文档合并到右侧】 font_size = 字号 underscore = 下划线 right_intent = 右缩进 left_intent = 左缩进 subscript = 下标 superscript = 上标 clean_format = 清空格式 add_video = 添加视频 formula = 公式 font_color = 字体颜色 bg_color = 背景颜色 input_pwd = 请输入密码 read_pwd = 浏览密码 commit = 提交 ft_author = 作者: ft_last_editor = 最后编辑: ft_create_time = 创建时间: ft_update_time = 更新时间: view_count = 阅读次数 changetheme = 切换主题 prev = 上一篇 next = 下一篇 [project] prj_space_list = 项目空间列表 prj_space_list_of = 项目空间%s的项目列表 search_title = 显示项目空间为"%s"的项目 author = 作者 no_project = 暂无项目 prj_amount = 项目数量 creator = 创建人 create_time = 创建时间 no_project_space = 没有项目空间 [search] title = 搜索 search_title = 显示"%s"的搜索结果 doc = 文档 prj = 项目 blog = 文章 from_proj = 来自项目 from_blog = 来自文章 author = 作者 update_time = 更新时间 no_result = 暂无相关搜索结果 [page] first = 首页 last = 末页 prev = 上一页 next = 下一页 [uc] user_center = 用户中心 base_info = 基本信息 change_pwd = 修改密码 username = 用户名 nickname = 昵称 realname = 真实姓名 email = 邮箱 mobile = 手机号 description = 描述 description_tips = 描述不能超过500字 avatar = 头像 change_avatar = 修改头像 password = 密码 origin_pwd = 原始密码 new_pwd = 新密码 confirm_pwd = 确认密码 role = 角色 type = 类型 status = 状态 super_admin = 超级管理员 admin = 管理员 user = 普通用户 read_usr = 只读用户 normal = 正常 disable = 禁用 enable = 启用 create_user = 创建用户 edit_user = 编辑用户 pwd_tips = 不修改密码请留空,只支持本地用户修改密码 [mgr] language = 默认语言 zh_cn = 简体中文 en_us = English dashboard_menu = 仪表盘 user_menu = 用户管理 team_menu = 团队管理 project_menu = 项目管理 project_space_menu = 项目空间管理 comment_menu = 评论管理 config_menu = 配置管理 attachment_menu = 附件管理 label_menu = 标签管理 dashboard_mgr = 仪表盘 user_mgr = 用户管理 team_mgr = 团队管理 project_mgr = 项目管理 project_space_mgr = 项目空间管理 comment_mgr = 评论管理 config_mgr = 配置管理 config_file = 配置文件 attachment_mgr = 附件管理 label_mgr = 标签管理 label_name = 标签名称 used_quantity = 使用数量 proj_amount = 项目数量 blog_amount = 文章数量 member_amount = 成员数量 comment_amount = 评论数量 attachment_amount = 附件数量 member_mgr = 成员管理 add_member = 添加成员 create_team = 创建团队 team_name = 团队名称 proj = 项目 member = 成员 edit_team = 编辑团队 team_member_mgr = 团队用户管理 team_proj = 团队项目 add_proj = 添加项目 proj_name = 项目名称 proj_author = 项目作者 join_time = 加入时间 join_proj = 加入项目 file_name = 文件名称 is_exist = 是否存在 exist = 存在 deleted = 已删除 proj_blog_name = 项目/文章名称 doc_name = 文档名称 file_path = 文件路径 download_url = 下载路径 file_size = 文件大小 upload_time = 上传时间 download = 下载 download_title = 下载到本地 attachment_name = 附件名称 site_name = 网站标题 domain_icp = 域名备案 site_desc = 网站描述 site_desc_tips = 描述信息不超过500个字符 enable_anonymous_access = 启用匿名访问 enable = 开启 disable = 关闭 enable_register = 启用注册 enable_captcha = 启用验证码 enable_doc_his = 启用文档历史 proj_space_name = 项目空间名称 proj_space_id = 项目空间标识 create_proj_space = 创建项目空间 edit_proj_space = 编辑项目空间 proj_list = 项目列表 edit_proj = 编辑项目 create_time = 创建时间 creator = 创建者 doc_amount = 文档数量 last_edit = 最后编辑 delete_project = 删除项目 ================================================ FILE: conf/mail.go ================================================ package conf import ( "strings" "github.com/beego/beego/v2/server/web" ) type SmtpConf struct { EnableMail bool MailNumber int SmtpUserName string SmtpHost string SmtpPassword string SmtpPort int FormUserName string MailExpired int Secure string } func GetMailConfig() *SmtpConf { user_name, _ := web.AppConfig.String("smtp_user_name") password, _ := web.AppConfig.String("smtp_password") smtp_host, _ := web.AppConfig.String("smtp_host") smtp_port := web.AppConfig.DefaultInt("smtp_port", 25) form_user_name, _ := web.AppConfig.String("form_user_name") enable_mail, _ := web.AppConfig.String("enable_mail") mail_number := web.AppConfig.DefaultInt("mail_number", 5) secure := web.AppConfig.DefaultString("secure", "NONE") if secure != "NONE" && secure != "LOGIN" && secure != "SSL" { secure = "NONE" } c := &SmtpConf{ EnableMail: strings.EqualFold(enable_mail, "true"), MailNumber: mail_number, SmtpUserName: user_name, SmtpHost: smtp_host, SmtpPassword: password, FormUserName: form_user_name, SmtpPort: smtp_port, Secure: secure, } return c } ================================================ FILE: conf/workweixin.go ================================================ package conf import ( "github.com/beego/beego/v2/server/web" ) type WorkWeixinConf struct { CorpId string // 企业ID AgentId string // 应用ID Secret string // 应用密钥 // ContactSecret string // 通讯录密钥 } func GetWorkWeixinConfig() *WorkWeixinConf { corpid, _ := web.AppConfig.String("workweixin_corpid") agentid, _ := web.AppConfig.String("workweixin_agentid") secret, _ := web.AppConfig.String("workweixin_secret") // contact_secret, _ := web.AppConfig.String("workweixin_contact_secret") c := &WorkWeixinConf{ CorpId: corpid, AgentId: agentid, Secret: secret, // ContactSecret: contact_secret, } return c } ================================================ FILE: controllers/AccountController.go ================================================ package controllers import ( "context" "encoding/json" "errors" "github.com/mindoc-org/mindoc/cache" "github.com/mindoc-org/mindoc/utils/auth2" "github.com/mindoc-org/mindoc/utils/auth2/dingtalk" "github.com/mindoc-org/mindoc/utils/auth2/wecom" "html/template" "math/rand" "net/http" "net/url" "regexp" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/lifei6671/gocaptcha" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/mail" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" ) const ( SessionUserInfoKey = "session-user-info-key" AccessTokenCacheKey = "access-token-cache-key" ) var src = rand.New(rand.NewSource(time.Now().UnixNano())) // AccountController 用户登录与注册 type AccountController struct { BaseController } func (c *AccountController) referer() string { u, _ := url.PathUnescape(c.GetString("url")) if u == "" { u = conf.URLFor("HomeController.Index") } return u } func (c *AccountController) IsInWorkWeixin() bool { ua := c.Ctx.Input.UserAgent() var wechatRule = regexp.MustCompile(`\bMicroMessenger\/\d+(\.\d+)*\b`) var wxworkRule = regexp.MustCompile(`\bwxwork\/\d+(\.\d+)*\b`) return wechatRule.MatchString(ua) && wxworkRule.MatchString(ua) } func (c *AccountController) Prepare() { c.BaseController.Prepare() c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true) c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML()) c.Data["CanLoginWorkWeixin"] = len(web.AppConfig.DefaultString("workweixin_corpid", "")) > 0 c.Data["CanLoginDingTalk"] = len(web.AppConfig.DefaultString("dingtalk_app_key", "")) > 0 if !c.EnableXSRF { return } if c.Ctx.Input.IsPost() { token := c.Ctx.Input.Query("_xsrf") if token == "" { token = c.Ctx.Request.Header.Get("X-Xsrftoken") } if token == "" { token = c.Ctx.Request.Header.Get("X-Csrftoken") } if token == "" { if c.IsAjax() { c.JsonResult(403, i18n.Tr(c.Lang, "message.illegal_request")) } else { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.illegal_request")) } } xsrfToken := c.XSRFToken() if xsrfToken != token { if c.IsAjax() { c.JsonResult(403, i18n.Tr(c.Lang, "message.illegal_request")) } else { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.illegal_request")) } } } } // Login 用户登录 func (c *AccountController) Login() { c.TplName = "account/login.tpl" if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { u := c.GetString("url") if u == "" { u = c.Ctx.Request.Header.Get("Referer") } if u == "" { u = conf.URLFor("HomeController.Index") } c.Redirect(u, 302) } var remember CookieRemember // 如果 Cookie 中存在登录信息 if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { if err := utils.Decode(cookie, &remember); err == nil { if member, err := models.NewMember().Find(remember.MemberId); err == nil { c.SetMember(*member) c.LoggedIn(false) c.StopRun() } } } if c.Ctx.Input.IsPost() { account := c.GetString("account") password := c.GetString("password") captcha := c.GetString("code") isRemember := c.GetString("is_remember") // 如果开启了验证码 if v, ok := c.Option["ENABLED_CAPTCHA"]; ok && strings.EqualFold(v, "true") { v, ok := c.GetSession(conf.CaptchaSessionName).(string) if !ok || !strings.EqualFold(v, captcha) { c.JsonResult(6001, i18n.Tr(c.Lang, "message.captcha_wrong")) } } if account == "" || password == "" { c.JsonResult(6002, i18n.Tr(c.Lang, "message.account_or_password_empty")) } member, err := models.NewMember().Login(account, password) if err == nil { member.LastLoginTime = time.Now() _ = member.Update("last_login_time") c.SetMember(*member) if strings.EqualFold(isRemember, "yes") { remember.MemberId = member.MemberId remember.Account = member.Account remember.Time = time.Now() v, err := utils.Encode(remember) if err == nil { c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30).Unix()) } } c.JsonResult(0, "ok", c.referer()) } else { logs.Error("用户登录 ->", err) c.JsonResult(500, i18n.Tr(c.Lang, "message.wrong_account_password"), nil) } return } referer := c.referer() u := c.GetString("url") if u == "" { u = referer if u == "" { u = conf.BaseUrl } } else { var schemaRule = regexp.MustCompile(`^https?\:\/\/`) if !schemaRule.MatchString(u) { u = conf.BaseUrl + u } } c.Data["url"] = referer auth2Redirect := "AccountController.Auth2Redirect" if can, _ := c.Data["CanLoginWorkWeixin"].(bool); can { c.Data["workweixin_login_url"] = conf.URLFor(auth2Redirect, ":app", wecom.AppName, "url", url.PathEscape(u)) } if can, _ := c.Data["CanLoginDingTalk"].(bool); can { c.Data["dingtalk_login_url"] = conf.URLFor(auth2Redirect, ":app", dingtalk.AppName, "url", url.PathEscape(u)) } return } /* Auth2.0 第三方对接思路: 1. Auth2Redirect: 点击相应第三方接口,路由重定向至第三方提供的Auth2.0地址 2. Auth2Callback: 第三方回调处理,接收回调的授权码,并获取用户信息 已绑定: 则读取用户信息,直接登录 未绑定: 则弹窗提示(需要敏感信息) a) Auth2BindAccount: 绑定已有账户(用户名+密码) b) Auth2AutoAccount: 自动创建账户,以第三方用户ID作为用户名,密码123456。 用该方式创建的账户,无法使用账号密码登录,需要修改一次密码后才可以进行账号密码登录。 */ func (c *AccountController) getAuth2Client() (auth2.Client, error) { app := c.Ctx.Input.Param(":app") var client auth2.Client tokenKey := AccessTokenCacheKey + "-" + app switch app { case wecom.AppName: if can, _ := c.Data["CanLoginWorkWeixin"].(bool); !can { return nil, errors.New("auth2.client.wecom.disabled") } corpId, _ := web.AppConfig.String("workweixin_corpid") agentId, _ := web.AppConfig.String("workweixin_agentid") secret, _ := web.AppConfig.String("workweixin_secret") client = wecom.NewClient(corpId, agentId, secret) case dingtalk.AppName: if can, _ := c.Data["CanLoginDingTalk"].(bool); !can { return nil, errors.New("auth2.client.dingtalk.disabled") } appKey, _ := web.AppConfig.String("dingtalk_app_key") appSecret, _ := web.AppConfig.String("dingtalk_app_secret") client = dingtalk.NewClient(appSecret, appKey) default: return nil, errors.New("auth2.client.notsupported") } var tokenCache auth2.AccessTokenCache err := cache.Get(tokenKey, &tokenCache) if err != nil { logs.Info("AccessToken从缓存读取失败") token, err := client.GetAccessToken(context.Background()) if err != nil { return client, nil } tokenCache = auth2.NewAccessToken(token) cache.Put(tokenKey, tokenCache, tokenCache.GetExpireIn()) } // 处理过期Token if tokenCache.IsExpired() { token, err := client.GetAccessToken(context.Background()) if err != nil { return client, nil } tokenCache = auth2.NewAccessToken(token) cache.Put(tokenKey, tokenCache, tokenCache.GetExpireIn()) } client.SetAccessToken(tokenCache) return client, nil } func (c *AccountController) parseAuth2CallbackParam() (code, state string) { switch c.Ctx.Input.Param(":app") { case wecom.AppName: code = c.GetString("code") state = c.GetString("state") case dingtalk.AppName: code = c.GetString("authCode") state = c.GetString("state") } logs.Debug("code: ", code) logs.Debug("state: ", state) return } func (c *AccountController) getAuth2Account() (models.Auth2Account, error) { switch c.Ctx.Input.Param(":app") { case wecom.AppName: return models.NewWorkWeixinAccount(), nil case dingtalk.AppName: return models.NewDingTalkAccount(), nil } return nil, errors.New("auth2.account.notsupported") } // Auth2Redirect 第三方auth2.0登录: 钉钉、企业微信 func (c *AccountController) Auth2Redirect() { client, err := c.getAuth2Client() if err != nil { c.DelSession(conf.LoginSessionName) c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) c.StopRun() return } app := c.Ctx.Input.Param(":app") var isAppBrowser bool switch app { case wecom.AppName: isAppBrowser = c.IsInWorkWeixin() } var callback string u := c.GetString("url") if u == "" { u = c.referer() callback = conf.URLFor("AccountController.Auth2Callback", ":app", app) } if u != "" { var schemaRule = regexp.MustCompile(`^https?\:\/\/`) if !schemaRule.MatchString(u) { u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/") } callback = conf.URLFor("AccountController.Auth2Callback", ":app", app, "url", url.PathEscape(u)) } logs.Debug("callback: ", callback) // debug c.Redirect(client.BuildURL(callback, isAppBrowser), http.StatusFound) } // Auth2Callback 第三方auth2.0回调 func (c *AccountController) Auth2Callback() { client, err := c.getAuth2Client() if err != nil { c.DelSession(conf.LoginSessionName) c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) c.StopRun() logs.Error(err) return } if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { u := c.GetString("url") if u == "" { u = c.Ctx.Request.Header.Get("Referer") } if u == "" { u = conf.URLFor("HomeController.Index") } member, err := models.NewMember().Find(member.MemberId) if err != nil { c.DelSession(conf.LoginSessionName) c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) } else { c.SetMember(*member) } c.Redirect(u, 302) } var remember CookieRemember // 如果 Cookie 中存在登录信息 if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { if err := utils.Decode(cookie, &remember); err == nil { if member, err := models.NewMember().Find(remember.MemberId); err == nil { c.SetMember(*member) c.LoggedIn(false) c.StopRun() } } } c.TplName = "account/auth2_callback.tpl" bindExisted := "false" errMsg := "" userInfoJson := "{}" defer func() { c.Data["bind_existed"] = template.JS(bindExisted) logs.Debug("bind_existed: ", bindExisted) c.Data["error_msg"] = template.JS(errMsg) c.Data["user_info_json"] = template.JS(userInfoJson) c.Data["app"] = template.JS(c.Ctx.Input.Param(":app")) }() // 请求参数获取 code, state := c.parseAuth2CallbackParam() if err := client.ValidateCallback(state); err != nil { c.DelSession(conf.LoginSessionName) c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) errMsg = err.Error() logs.Error(err) return } userInfo, err := client.GetUserInfo(context.Background(), code) if err != nil { c.DelSession(conf.LoginSessionName) c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) errMsg = err.Error() logs.Error(err) return } account, err := c.getAuth2Account() if err != nil { logs.Error("获取Auth2用户失败 ->", err) c.JsonResult(500, "不支持的第三方用户", nil) return } member, err := account.ExistedMember(userInfo.UserId) if err != nil { if err == orm.ErrNoRows { if userInfo.Mobile == "" { errMsg = "请到应用浏览器中登录,并授权获取敏感信息。" } else { jsonInfo, _ := json.Marshal(userInfo) userInfoJson = string(jsonInfo) errMsg = "" c.SetSession(SessionUserInfoKey, userInfo) } } else { logs.Error("Error: ", err) errMsg = "登录错误: " + err.Error() } return } bindExisted = "true" errMsg = "" member.LastLoginTime = time.Now() _ = member.Update("last_login_time") c.SetMember(*member) remember.MemberId = member.MemberId remember.Account = member.Account remember.Time = time.Now() v, err := utils.Encode(remember) if err == nil { c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) } u := c.GetString("url") if u == "" { u = conf.URLFor("HomeController.Index") } c.Redirect(u, 302) } // Auth2BindAccount 第三方auth2.0绑定已有账号 func (c *AccountController) Auth2BindAccount() { userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo) if !ok || len(userInfo.UserId) <= 0 { c.DelSession(SessionUserInfoKey) c.JsonResult(400, "请求错误, 请从首页重新登录") return } account := c.GetString("account") password := c.GetString("password") if account == "" || password == "" { c.JsonResult(400, "账号或密码不能为空") return } member, err := models.NewMember().Login(account, password) if err != nil { logs.Error("用户登录 ->", err) c.JsonResult(500, "账号或密码错误", nil) return } bindAccount, err := c.getAuth2Account() if err != nil { logs.Error("获取Auth2用户失败 ->", err) c.JsonResult(500, "不支持的第三方用户", nil) return } member.CreateAt = 0 ormer := orm.NewOrm() o, err := ormer.Begin() if err != nil { logs.Error("开启事务时出错 -> ", err) c.JsonResult(500, "开启事务时出错: ", err.Error()) return } if err := bindAccount.AddBind(ormer, userInfo, member); err != nil { logs.Error(err) o.Rollback() c.JsonResult(500, "绑定失败,数据库错误: "+err.Error()) return } // 绑定成功之后修改用户信息 member.LastLoginTime = time.Now() //member.RealName = user_info.Name //member.Avatar = user_info.Avatar if len(member.Avatar) < 1 { member.Avatar = conf.GetDefaultAvatar() } //member.Email = user_info.Email //member.Phone = user_info.Mobile if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil { o.Rollback() logs.Error("保存用户信息失败=>", err) c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error()) return } if err := o.Commit(); err != nil { logs.Error("开启事务时出错 -> ", err) c.JsonResult(500, "开启事务时出错: ", err.Error()) return } c.DelSession(SessionUserInfoKey) c.SetMember(*member) var remember CookieRemember remember.MemberId = member.MemberId remember.Account = member.Account remember.Time = time.Now() v, err := utils.Encode(remember) if err != nil { c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) return } c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) c.JsonResult(0, "绑定成功", nil) } // Auth2AutoAccount auth2.0自动创建账号 func (c *AccountController) Auth2AutoAccount() { app := c.Ctx.Input.Param(":app") logs.Debug("app: ", app) userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo) if !ok || len(userInfo.UserId) <= 0 { c.DelSession(SessionUserInfoKey) c.JsonResult(400, "请求错误, 请从首页重新登录") return } c.DelSession(SessionUserInfoKey) member := models.NewMember() if _, err := member.FindByAccount(userInfo.UserId); err == nil && member.MemberId > 0 { c.JsonResult(400, "账号已存在") return } ormer := orm.NewOrm() o, err := ormer.Begin() if err != nil { logs.Error("开启事务时出错 -> ", err) c.JsonResult(500, "开启事务时出错: ", err.Error()) return } member.Account = userInfo.UserId member.RealName = userInfo.Name member.Password = "123456" // 强制设置默认密码,需修改一次密码后,才可以进行账号密码登录 hash, err := utils.PasswordHash(member.Password) if err != nil { logs.Error("加密用户密码失败 =>", err) c.JsonResult(500, "加密用户密码失败"+err.Error()) return } logs.Debug("member.Password: ", member.Password) logs.Debug("hash: ", hash) member.Password = hash member.Role = conf.MemberGeneralRole member.Avatar = userInfo.Avatar if len(member.Avatar) < 1 { member.Avatar = conf.GetDefaultAvatar() } member.CreateAt = 0 member.Email = userInfo.Mail member.Phone = userInfo.Mobile member.Status = 0 if _, err = ormer.Insert(member); err != nil { o.Rollback() c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) return } account, err := c.getAuth2Account() if err != nil { logs.Error("获取Auth2用户失败 ->", err) c.JsonResult(500, "不支持的第三方用户", nil) return } member.CreateAt = 0 if err := account.AddBind(ormer, userInfo, member); err != nil { logs.Error(err) o.Rollback() c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) return } if err := o.Commit(); err != nil { logs.Error("提交事务时出错 -> ", err) c.JsonResult(500, "提交事务时出错: ", err.Error()) return } member.LastLoginTime = time.Now() _ = member.Update("last_login_time") c.SetMember(*member) var remember CookieRemember remember.MemberId = member.MemberId remember.Account = member.Account remember.Time = time.Now() v, err := utils.Encode(remember) if err != nil { c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) return } c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) c.JsonResult(0, "绑定成功", nil) } // 钉钉登录 //func (c *AccountController) DingTalkLogin() { // code := c.GetString("dingtalk_code") // if code == "" { // c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_obtain_user_info"), nil) // } // // appKey, _ := web.AppConfig.String("dingtalk_app_key") // appSecret, _ := web.AppConfig.String("dingtalk_app_secret") // tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader") // // if appKey == "" || appSecret == "" || tmpReader == "" { // c.JsonResult(500, i18n.Tr(c.Lang, "message.dingtalk_auto_login_not_enable"), nil) // c.StopRun() // } // // dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey) // err := dingtalkAgent.GetAccesstoken() // if err != nil { // logs.Warn("获取钉钉临时Token失败 ->", err) // c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) // c.StopRun() // } // // userid, err := dingtalkAgent.GetUserIDByCode(code) // if err != nil { // logs.Warn("获取钉钉用户ID失败 ->", err) // c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) // c.StopRun() // } // // username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) // if err != nil { // logs.Warn("获取钉钉用户信息失败 ->", err) // c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) // c.StopRun() // } // // member, err := models.NewMember().TmpLogin(tmpReader) // if err == nil { // member.LastLoginTime = time.Now() // _ = member.Update("last_login_time") // member.Account = username // if avatar != "" { // member.Avatar = avatar // } // // c.SetMember(*member) // } // c.JsonResult(0, "ok", username) //} // WorkWeixinLogin 用户企业微信登录 //func (c *AccountController) WorkWeixinLogin() { // logs.Info("UserAgent: ", c.Ctx.Input.UserAgent()) // debug // // if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { // u := c.GetString("url") // if u == "" { // u = c.Ctx.Request.Header.Get("Referer") // if u == "" { // u = conf.URLFor("HomeController.Index") // } // } // // session自动登录时刷新session内容 // member, err := models.NewMember().Find(member.MemberId) // if err != nil { // c.DelSession(conf.LoginSessionName) // c.SetMember(models.Member{}) // c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) // } else { // c.SetMember(*member) // } // c.Redirect(u, 302) // } // var remember CookieRemember // // 如果 Cookie 中存在登录信息 // if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { // if err := utils.Decode(cookie, &remember); err == nil { // if member, err := models.NewMember().Find(remember.MemberId); err == nil { // c.SetMember(*member) // c.LoggedIn(false) // c.StopRun() // } // } // } // // if c.Ctx.Input.IsPost() { // // account := c.GetString("account") // // password := c.GetString("password") // // captcha := c.GetString("code") // // isRemember := c.GetString("is_remember") // c.JsonResult(400, "request method not allowed", nil) // } else { // var callback_u string // u := c.GetString("url") // if u == "" { // u = c.referer() // } // if u != "" { // var schemaRule = regexp.MustCompile(`^https?\:\/\/`) // if !schemaRule.MatchString(u) { // u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/") // } // } // if u == "" { // callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback") // } else { // callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback", "url", url.PathEscape(u)) // } // logs.Info("callback_u: ", callback_u) // debug // // state := "mindoc" // workweixinConf := conf.GetWorkWeixinConfig() // appid := workweixinConf.CorpId // agentid := workweixinConf.AgentId // var redirect_uri string // // isInWorkWeixin := c.IsInWorkWeixin() // c.Data["IsInWorkWeixin"] = isInWorkWeixin // if isInWorkWeixin { // // 企业微信内-网页授权登录 // urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect" // redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_AuthorizeUrlBase, appid, agentid, url.PathEscape(callback_u), state) // } else { // // 浏览器内-扫码授权登录 // urlFmt := "%s?login_type=CorpApp&appid=%s&agentid=%s&redirect_uri=%s&state=%s" // redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_QRConnectUrlBase, appid, agentid, url.PathEscape(callback_u), state) // } // logs.Info("redirect_uri: ", redirect_uri) // debug // c.Redirect(redirect_uri, 302) // } //} /* 思路: 1. 浏览器打开 用户名+密码 登录 与企业微信没有交集 手机企业微信登录->扫码页面->扫码后获取用户信息, 判断是否绑定了企业微信 已绑定,则读取用户信息,直接登录 未绑定,则弹窗提示[未绑定企业微信,请先在企业微信中打开,完成绑定] 2. 企业微信打开->自动登录->判断是否绑定了企业微信 已绑定,则读取用户信息,直接登录 未绑定,则弹窗提示 是否已有账户(用户名+密码方式) 有: 弹窗输入[用户名+密码+验证码]校验 无: 直接以企业UserId作为用户名(小写),创建随机密码 */ // WorkWeixinLoginCallback 用户企业微信登录-回调 //func (c *AccountController) WorkWeixinLoginCallback() { // c.TplName = "account/auth2_callback.tpl" // // if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { // u := c.GetString("url") // if u == "" { // u = c.Ctx.Request.Header.Get("Referer") // } // if u == "" { // u = conf.URLFor("HomeController.Index") // } // member, err := models.NewMember().Find(member.MemberId) // if err != nil { // c.DelSession(conf.LoginSessionName) // c.SetMember(models.Member{}) // c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) // } else { // c.SetMember(*member) // } // c.Redirect(u, 302) // } // // var remember CookieRemember // // 如果 Cookie 中存在登录信息 // if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { // if err := utils.Decode(cookie, &remember); err == nil { // if member, err := models.NewMember().Find(remember.MemberId); err == nil { // c.SetMember(*member) // c.LoggedIn(false) // c.StopRun() // } // } // } // // // 请求参数获取 // req_code := c.GetString("code") // logs.Warning("req_code: ", req_code) // req_state := c.GetString("state") // logs.Warning("req_state: ", req_state) // var user_info_json string // var error_msg string // var bind_existed string // if len(req_code) > 0 && req_state == "mindoc" { // // 获取当前应用的access_token // access_token, ok := workweixin.GetAccessToken() // if ok { // logs.Warning("access_token: ", access_token) // // 获取当前请求的userid // user_id, ticket, ok := workweixin.RequestUserId(access_token, req_code) // if ok { // logs.Warning("user_id: ", user_id) // // 查询系统现有数据,是否绑定了当前请求用户的企业微信 // member, err := models.NewWorkWeixinAccount().ExistedMember(user_id) // if err == nil { // member.LastLoginTime = time.Now() // _ = member.Update("last_login_time") // // c.SetMember(*member) // // var remember CookieRemember // remember.MemberId = member.MemberId // remember.Account = member.Account // remember.Time = time.Now() // v, err := utils.Encode(remember) // if err == nil { // c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) // } // bind_existed = "true" // error_msg = "" // u := c.GetString("url") // if u == "" { // u = conf.URLFor("HomeController.Index") // } // c.Redirect(u, 302) // } else if err == orm.ErrNoRows { // bind_existed = "false" // if ticket == "" { // error_msg = "请到企业微信中登录,并授权获取敏感信息。" // } else { // user_info, err := workweixin.RequestUserPrivateInfo(access_token, user_id, ticket) // if err != nil { // error_msg = "获取敏感信息错误: " + err.Error() // } else { // json_info, _ := json.Marshal(user_info) // user_info_json = string(json_info) // error_msg = "" // c.SetSession(SessionUserInfoKey, user_info) // } // } // } else { // logs.Error("Error: ", err) // error_msg = "登录错误: " + err.Error() // } // } else { // error_msg = "获取用户Id失败: " + user_id // } // } else { // error_msg = "应用凭据获取失败: " + access_token // } // } else { // error_msg = "参数错误" // } // if user_info_json == "" { // user_info_json = "{}" // } // if bind_existed == "" { // bind_existed = "null" // } // // refer & doc: // // - https://golang.org/pkg/html/template/#HTML // // - https://stackoverflow.com/questions/24411880/go-html-templates-can-i-stop-the-templates-package-inserting-quotes-around-stri // // - https://stackoverflow.com/questions/38035176/insert-javascript-snippet-inside-template-with-beego-golang // c.Data["bind_existed"] = template.JS(bind_existed) // logs.Debug("bind_existed: ", bind_existed) // c.Data["error_msg"] = template.JS(error_msg) // c.Data["user_info_json"] = template.JS(user_info_json) // /* // // 调试: 显示源码 // result, err := c.RenderString() // if err != nil { // logs.Error(err) // } else { // logs.Warning(result) // } // */ //} // WorkWeixinLoginBind 用户企业微信登录-绑定 //func (c *AccountController) WorkWeixinLoginBind() { // if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserPrivateInfo); ok && len(user_info.UserId) > 0 { // req_account := c.GetString("account") // req_password := c.GetString("password") // if req_account == "" || req_password == "" { // c.JsonResult(400, "账号或密码不能为空") // } else { // member, err := models.NewMember().Login(req_account, req_password) // if err == nil { // account := models.NewWorkWeixinAccount() // account.MemberId = member.MemberId // account.WorkWeixin_UserId = user_info.UserId // member.CreateAt = 0 // ormer := orm.NewOrm() // o, err := ormer.Begin() // if err != nil { // logs.Error("开启事务时出错 -> ", err) // c.JsonResult(500, "开启事务时出错: ", err.Error()) // } // if err := account.AddBind(ormer); err != nil { // o.Rollback() // c.JsonResult(500, "绑定失败,数据库错误: "+err.Error()) // } else { // // 绑定成功之后修改用户信息 // member.LastLoginTime = time.Now() // //member.RealName = user_info.Name // //member.Avatar = user_info.Avatar // if len(member.Avatar) < 1 { // member.Avatar = conf.GetDefaultAvatar() // } // //member.Email = user_info.Email // //member.Phone = user_info.Mobile // if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil { // o.Rollback() // logs.Error("保存用户信息失败=>", err) // c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error()) // } else { // if err := o.Commit(); err != nil { // logs.Error("开启事务时出错 -> ", err) // c.JsonResult(500, "开启事务时出错: ", err.Error()) // } else { // c.DelSession(SessionUserInfoKey) // c.SetMember(*member) // // var remember CookieRemember // remember.MemberId = member.MemberId // remember.Account = member.Account // remember.Time = time.Now() // v, err := utils.Encode(remember) // if err == nil { // c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) // c.JsonResult(0, "绑定成功", nil) // } else { // c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) // } // } // } // // } // // } else { // logs.Error("用户登录 ->", err) // c.JsonResult(500, "账号或密码错误", nil) // } // c.JsonResult(500, "TODO: 绑定以后账号功能开发中") // } // } else { // if ok { // c.DelSession(SessionUserInfoKey) // } // c.JsonResult(400, "请求错误, 请从首页重新登录") // } // //} // WorkWeixinLoginIgnore 用户企业微信登录-忽略 //func (c *AccountController) WorkWeixinLoginIgnore() { // if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserPrivateInfo); ok && len(user_info.UserId) > 0 { // c.DelSession(SessionUserInfoKey) // member := models.NewMember() // // if _, err := member.FindByAccount(user_info.UserId); err == nil && member.MemberId > 0 { // c.JsonResult(400, "账号已存在") // } // // ormer := orm.NewOrm() // o, err := ormer.Begin() // if err != nil { // logs.Error("开启事务时出错 -> ", err) // c.JsonResult(500, "开启事务时出错: ", err.Error()) // } // // member.Account = user_info.UserId // member.RealName = user_info.Name // var rnd = rand.New(src) // // fmt.Sprintf("%x", rnd.Uint64()) // // strconv.FormatUint(rnd.Uint64(), 16) // member.Password = user_info.UserId + strconv.FormatUint(rnd.Uint64(), 16) // member.Password = "123456" // 强制设置默认密码,需修改一次密码后,才可以进行账号密码登录 // hash, err := utils.PasswordHash(member.Password) // if err != nil { // logs.Error("加密用户密码失败 =>", err) // c.JsonResult(500, "加密用户密码失败"+err.Error()) // } else { // logs.Error("member.Password: ", member.Password) // logs.Error("hash: ", hash) // member.Password = hash // } // member.Role = conf.MemberGeneralRole // member.Avatar = user_info.Avatar // if len(member.Avatar) < 1 { // member.Avatar = conf.GetDefaultAvatar() // } // member.CreateAt = 0 // member.Email = user_info.BizMail // member.Phone = user_info.Mobile // member.Status = 0 // if _, err = ormer.Insert(member); err != nil { // o.Rollback() // c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) // } else { // account := models.NewWorkWeixinAccount() // account.MemberId = member.MemberId // account.WorkWeixin_UserId = user_info.UserId // member.CreateAt = 0 // if err := account.AddBind(ormer); err != nil { // o.Rollback() // c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) // } else { // if err := o.Commit(); err != nil { // logs.Error("提交事务时出错 -> ", err) // c.JsonResult(500, "提交事务时出错: ", err.Error()) // } else { // member.LastLoginTime = time.Now() // _ = member.Update("last_login_time") // // c.SetMember(*member) // // var remember CookieRemember // remember.MemberId = member.MemberId // remember.Account = member.Account // remember.Time = time.Now() // v, err := utils.Encode(remember) // if err == nil { // c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) // c.JsonResult(0, "绑定成功", nil) // } else { // c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) // } // } // } // } // } else { // if ok { // c.DelSession(SessionUserInfoKey) // } // c.JsonResult(400, "请求错误, 请从首页重新登录") // } //} // QR二维码登录 //func (c *AccountController) QRLogin() { // appName := c.Ctx.Input.Param(":app") // // switch appName { // // 钉钉扫码登录 // case "dingtalk": // code := c.GetString("code") // state := c.GetString("state") // if state != "1" || code == "" { // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } // appKey, _ := web.AppConfig.String("dingtalk_qr_key") // appSecret, _ := web.AppConfig.String("dingtalk_qr_secret") // // qrDingtalk := dingtalk.NewDingtalkQRLogin(appSecret, appKey) // unionID, err := qrDingtalk.GetUnionIDByCode(code) // if err != nil { // logs.Warn("获取钉钉临时UnionID失败 ->", err) // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } // // appKey, _ = web.AppConfig.String("dingtalk_app_key") // appSecret, _ = web.AppConfig.String("dingtalk_app_secret") // tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader") // // dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey) // err = dingtalkAgent.GetAccesstoken() // if err != nil { // logs.Warn("获取钉钉临时Token失败 ->", err) // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } // // userid, err := dingtalkAgent.GetUserIDByUnionID(unionID) // if err != nil { // logs.Warn("获取钉钉用户ID失败 ->", err) // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } // // username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) // if err != nil { // logs.Warn("获取钉钉用户信息失败 ->", err) // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } // // member, err := models.NewMember().TmpLogin(tmpReader) // if err == nil { // member.LastLoginTime = time.Now() // _ = member.Update("last_login_time") // member.Account = username // if avatar != "" { // member.Avatar = avatar // } // // c.SetMember(*member) // c.LoggedIn(false) // c.StopRun() // } // c.Redirect(conf.URLFor("AccountController.Login"), 302) // // // 企业微信扫码登录 // case "workweixin": // // // // default: // c.Redirect(conf.URLFor("AccountController.Login"), 302) // c.StopRun() // } //} // 登录成功后的操作,如重定向到原始请求页面 func (c *AccountController) LoggedIn(isPost bool) interface{} { turl := c.referer() if !isPost { c.Redirect(turl, 302) return nil } else { var data struct { TURL string `json:"url"` } data.TURL = turl return data } } // 用户注册 func (c *AccountController) Register() { c.TplName = "account/register.tpl" //如果用户登录了,则跳转到网站首页 if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { c.Redirect(conf.URLFor("HomeController.Index"), 302) } // 如果没有开启用户注册 if v, ok := c.Option["ENABLED_REGISTER"]; ok && !strings.EqualFold(v, "true") { c.Abort("404") } if c.Ctx.Input.IsPost() { account := c.GetString("account") password1 := c.GetString("password1") password2 := c.GetString("password2") email := c.GetString("email") captcha := c.GetString("code") if ok, err := regexp.MatchString(conf.RegexpAccount, account); account == "" || !ok || err != nil { c.JsonResult(6001, i18n.Tr(c.Lang, "message.username_invalid_format")) } if l := strings.Count(password1, ""); password1 == "" || l > 50 || l < 6 { c.JsonResult(6002, i18n.Tr(c.Lang, "message.password_length_invalid")) } if password1 != password2 { c.JsonResult(6003, i18n.Tr(c.Lang, "message.incorrect_confirm_password")) } if ok, err := regexp.MatchString(conf.RegexpEmail, email); !ok || err != nil || email == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.email_invalid_format")) } // 如果开启了验证码 if v, ok := c.Option["ENABLED_CAPTCHA"]; ok && strings.EqualFold(v, "true") { v, ok := c.GetSession(conf.CaptchaSessionName).(string) if !ok || !strings.EqualFold(v, captcha) { c.JsonResult(6001, i18n.Tr(c.Lang, "message.captcha_wrong")) } } member := models.NewMember() if _, err := member.FindByAccount(account); err == nil && member.MemberId > 0 { c.JsonResult(6005, i18n.Tr(c.Lang, "message.account_existed")) } member.Account = account member.Password = password1 member.Role = conf.MemberGeneralRole member.Avatar = conf.GetDefaultAvatar() member.CreateAt = 0 member.Email = email member.Status = 0 if err := member.Add(); err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed_register")) } c.JsonResult(0, "ok", member) } } // 找回密码 func (c *AccountController) FindPassword() { c.TplName = "account/find_password_setp1.tpl" mailConf := conf.GetMailConfig() if c.Ctx.Input.IsPost() { email := c.GetString("email") captcha := c.GetString("code") if email == "" { c.JsonResult(6005, i18n.Tr(c.Lang, "message.email_empty")) } if !mailConf.EnableMail { c.JsonResult(6004, i18n.Tr(c.Lang, "message.mail_service_not_enable")) } // 如果开启了验证码 if v, ok := c.Option["ENABLED_CAPTCHA"]; ok && strings.EqualFold(v, "true") { v, ok := c.GetSession(conf.CaptchaSessionName).(string) if !ok || !strings.EqualFold(v, captcha) { c.JsonResult(6001, i18n.Tr(c.Lang, "message.captcha_wrong")) } } member, err := models.NewMember().FindByFieldFirst("email", email) if err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.email_not_exist")) } if member == nil || member.Status != 0 { c.JsonResult(6007, i18n.Tr(c.Lang, "message.account_disable")) } if member == nil || member.AuthMethod == conf.AuthMethodLDAP { c.JsonResult(6011, i18n.Tr(c.Lang, "message.account_not_support_retrieval")) } count, err := models.NewMemberToken().FindSendCount(email, time.Now().Add(-1*time.Hour), time.Now()) if err != nil { logs.Error(err) c.JsonResult(6008, i18n.Tr(c.Lang, "message.failed_send_mail")) } if count > mailConf.MailNumber { c.JsonResult(6008, i18n.Tr(c.Lang, "message.sent_too_many_times")) } memberToken := models.NewMemberToken() memberToken.Token = string(utils.Krand(32, utils.KC_RAND_KIND_ALL)) memberToken.Email = email memberToken.MemberId = member.MemberId memberToken.IsValid = false if _, err := memberToken.InsertOrUpdate(); err != nil { c.JsonResult(6009, i18n.Tr(c.Lang, "message.failed_send_mail")) } data := map[string]interface{}{ "SITE_NAME": c.Option["SITE_NAME"], "url": conf.URLFor("AccountController.FindPassword", "token", memberToken.Token, "mail", email), "BaseUrl": c.BaseUrl(), } body, err := c.ExecuteViewPathTemplate("account/mail_template.tpl", data) if err != nil { logs.Error(err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed_send_mail")) } go func(mailConf *conf.SmtpConf, email string, body string) { mailConfig := &mail.SMTPConfig{ Username: mailConf.SmtpUserName, Password: mailConf.SmtpPassword, Host: mailConf.SmtpHost, Port: mailConf.SmtpPort, Secure: mailConf.Secure, Identity: "", } logs.Info(mailConfig) c := mail.NewSMTPClient(mailConfig) m := mail.NewMail() m.AddFrom(mailConf.FormUserName) m.AddFromName(mailConf.FormUserName) m.AddSubject("找回密码") m.AddHTML(body) m.AddTo(email) if e := c.Send(m); e != nil { logs.Error("发送邮件失败:" + e.Error()) } else { logs.Info("邮件发送成功:" + email) } //auth := smtp.PlainAuth( // "", // mail_conf.SmtpUserName, // mail_conf.SmtpPassword, // mail_conf.SmtpHost, //) // //mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" //subject := "Subject: 找回密码!\n" // //err = smtp.SendMail( // mail_conf.SmtpHost+":"+strconv.Itoa(mail_conf.SmtpPort), // auth, // mail_conf.FormUserName, // []string{email}, // []byte(subject+mime+"\n"+body), //) //if err != nil { // logs.Error("邮件发送失败 => ", email, err) //} }(mailConf, email, body) c.JsonResult(0, "ok", conf.URLFor("AccountController.Login")) } token := c.GetString("token") email := c.GetString("mail") if token != "" && email != "" { memberToken, err := models.NewMemberToken().FindByFieldFirst("token", token) if err != nil { logs.Error(err) c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.mail_expired") c.TplName = "errors/error.tpl" return } subTime := time.Until(memberToken.SendTime) if !strings.EqualFold(memberToken.Email, email) || subTime.Minutes() > float64(mailConf.MailExpired) || !memberToken.ValidTime.IsZero() { c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.captcha_expired") c.TplName = "errors/error.tpl" return } c.Data["Email"] = memberToken.Email c.Data["Token"] = memberToken.Token c.TplName = "account/find_password_setp2.tpl" } } // 校验邮件并修改密码 func (c *AccountController) ValidEmail() { password1 := c.GetString("password1") password2 := c.GetString("password2") captcha := c.GetString("code") token := c.GetString("token") email := c.GetString("mail") if password1 == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.password_empty")) } if l := strings.Count(password1, ""); l < 6 || l > 50 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.password_length_invalid")) } if password2 == "" { c.JsonResult(6002, i18n.Tr(c.Lang, "message.confirm_password_empty")) } if password1 != password2 { c.JsonResult(6003, i18n.Tr(c.Lang, "message.incorrect_confirm_password")) } if captcha == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.captcha_empty")) } v, ok := c.GetSession(conf.CaptchaSessionName).(string) if !ok || !strings.EqualFold(v, captcha) { c.JsonResult(6001, i18n.Tr(c.Lang, "message.captcha_wrong")) } mailConf := conf.GetMailConfig() memberToken, err := models.NewMemberToken().FindByFieldFirst("token", token) if err != nil { logs.Error(err) c.JsonResult(6007, i18n.Tr(c.Lang, "message.mail_expired")) } subTime := time.Until(memberToken.SendTime) if !strings.EqualFold(memberToken.Email, email) || subTime.Minutes() > float64(mailConf.MailExpired) || !memberToken.ValidTime.IsZero() { c.JsonResult(6008, i18n.Tr(c.Lang, "message.captcha_expired")) } member, err := models.NewMember().Find(memberToken.MemberId) if err != nil { logs.Error(err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.user_not_existed")) } hash, err := utils.PasswordHash(password1) if err != nil { logs.Error(err) c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed_save_password")) } member.Password = hash err = member.Update("password") memberToken.ValidTime = time.Now() memberToken.IsValid = true memberToken.InsertOrUpdate() if err != nil { logs.Error(err) c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed_save_password")) } c.JsonResult(0, "ok", conf.URLFor("AccountController.Login")) } // Logout 退出登录 func (c *AccountController) Logout() { c.SetMember(models.Member{}) c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) u := c.Ctx.Request.Header.Get("Referer") c.Redirect(conf.URLFor("AccountController.Login", "url", u), 302) } // 验证码 func (c *AccountController) Captcha() { captchaImage := gocaptcha.NewCaptchaImage(140, 40, gocaptcha.RandLightColor()) captchaImage.DrawNoise(gocaptcha.CaptchaComplexLower) // captchaImage.DrawTextNoise(gocaptcha.CaptchaComplexHigh) txt := gocaptcha.RandText(4) c.SetSession(conf.CaptchaSessionName, txt) captchaImage.DrawText(txt) // captchaImage.Drawline(3); captchaImage.DrawBorder(gocaptcha.ColorToRGB(0x17A7A7A)) // captchaImage.DrawHollowLine() captchaImage.SaveImage(c.Ctx.ResponseWriter, gocaptcha.ImageFormatJpeg) c.StopRun() } ================================================ FILE: controllers/BaseController.go ================================================ package controllers import ( "bytes" "encoding/json" "io" "strings" "time" "html/template" "io/ioutil" "path/filepath" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" ) type BaseController struct { web.Controller Member *models.Member Option map[string]string EnableAnonymous bool EnableDocumentHistory bool Lang string } type CookieRemember struct { MemberId int Account string Time time.Time } // Prepare 预处理. func (c *BaseController) Prepare() { c.Data["SiteName"] = "MinDoc" c.Data["Member"] = models.NewMember() controller, action := c.GetControllerAndAction() c.Data["ActionName"] = action c.Data["ControllerName"] = controller c.EnableAnonymous = false c.EnableDocumentHistory = false if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { c.Member = &member c.Data["Member"] = c.Member } else { var remember CookieRemember // //如果Cookie中存在登录信息,从cookie中获取用户信息 if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { if err := utils.Decode(cookie, &remember); err == nil { if member, err := models.NewMember().Find(remember.MemberId); err == nil { c.Member = member c.Data["Member"] = member c.SetMember(*member) } } } } conf.BaseUrl = c.BaseUrl() c.Data["BaseUrl"] = c.BaseUrl() if options, err := models.NewOption().All(); err == nil { c.Option = make(map[string]string, len(options)) for _, item := range options { c.Data[item.OptionName] = item.OptionValue c.Option[item.OptionName] = item.OptionValue } c.EnableAnonymous = strings.EqualFold(c.Option["ENABLE_ANONYMOUS"], "true") c.EnableDocumentHistory = strings.EqualFold(c.Option["ENABLE_DOCUMENT_HISTORY"], "true") } c.Data["HighlightStyle"] = web.AppConfig.DefaultString("highlight_style", "github") if b, err := ioutil.ReadFile(filepath.Join(web.BConfig.WebConfig.ViewsPath, "widgets", "scripts.tpl")); err == nil { c.Data["Scripts"] = template.HTML(string(b)) } c.SetLang() } // 判断用户是否登录. func (c *BaseController) isUserLoggedIn() bool { return c.Member != nil && c.Member.MemberId > 0 } // SetMember 获取或设置当前登录用户信息,如果 MemberId 小于 0 则标识删除 Session func (c *BaseController) SetMember(member models.Member) { if member.MemberId <= 0 { c.DelSession(conf.LoginSessionName) c.DelSession("uid") c.DestroySession() } else { c.SetSession(conf.LoginSessionName, member) c.SetSession("uid", member.MemberId) } } // JsonResult 响应 json 结果 func (c *BaseController) JsonResult(errCode int, errMsg string, data ...interface{}) { jsonData := make(map[string]interface{}, 3) jsonData["errcode"] = errCode jsonData["message"] = errMsg if len(data) > 0 && data[0] != nil { jsonData["data"] = data[0] } returnJSON, err := json.Marshal(jsonData) if err != nil { logs.Error(err) } c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache, no-store") _, err = io.WriteString(c.Ctx.ResponseWriter, string(returnJSON)) if err != nil { logs.Error(err) } c.StopRun() } // 如果错误不为空,则响应错误信息到浏览器. func (c *BaseController) CheckJsonError(code int, err error) { if err == nil { return } jsonData := make(map[string]interface{}, 3) jsonData["errcode"] = code jsonData["message"] = err.Error() returnJSON, err := json.Marshal(jsonData) if err != nil { logs.Error(err) } c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache, no-store") _, err = io.WriteString(c.Ctx.ResponseWriter, string(returnJSON)) if err != nil { logs.Error(err) } c.StopRun() } // ExecuteViewPathTemplate 执行指定的模板并返回执行结果. func (c *BaseController) ExecuteViewPathTemplate(tplName string, data interface{}) (string, error) { var buf bytes.Buffer viewPath := c.ViewPath if c.ViewPath == "" { viewPath = web.BConfig.WebConfig.ViewsPath } if err := web.ExecuteViewPathTemplate(&buf, tplName, viewPath, data); err != nil { return "", err } return buf.String(), nil } func (c *BaseController) BaseUrl() string { baseUrl := web.AppConfig.DefaultString("baseurl", "") if baseUrl != "" { if strings.HasSuffix(baseUrl, "/") { baseUrl = strings.TrimSuffix(baseUrl, "/") } } else { baseUrl = c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host } return baseUrl } // 显示错误信息页面. func (c *BaseController) ShowErrorPage(errCode int, errMsg string) { c.TplName = "errors/error.tpl" c.Data["ErrorMessage"] = errMsg c.Data["ErrorCode"] = errCode var buf bytes.Buffer exeData := map[string]interface{}{"ErrorMessage": errMsg, "ErrorCode": errCode, "BaseUrl": conf.BaseUrl, "Lang": c.Lang} if err := web.ExecuteViewPathTemplate(&buf, "errors/error.tpl", web.BConfig.WebConfig.ViewsPath, exeData); err != nil { c.Abort("500") } if errCode >= 200 && errCode <= 510 { c.CustomAbort(errCode, buf.String()) } else { c.CustomAbort(500, buf.String()) } } func (c *BaseController) CheckErrorResult(code int, err error) { if err != nil { c.ShowErrorPage(code, err.Error()) } } func (c *BaseController) SetLang() { hasCookie := false lang := c.GetString("lang") if len(lang) == 0 { lang = c.Ctx.GetCookie("lang") hasCookie = true } if len(lang) == 0 || !i18n.IsExist(lang) { if c.Data["language"] != nil { lang = c.Data["language"].(string) } else { lang, _ = web.AppConfig.String("default_lang") } } if !hasCookie { c.Ctx.SetCookie("lang", lang, 1<<31-1, "/") } c.Data["Lang"] = lang c.Lang = lang } ================================================ FILE: controllers/BlogController.go ================================================ package controllers import ( "context" "encoding/json" "fmt" "html/template" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/pagination" ) type BlogController struct { BaseController } func (c *BlogController) Prepare() { c.BaseController.Prepare() if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login")+"?url="+url.PathEscape(conf.BaseUrl+c.Ctx.Request.URL.RequestURI()), 302) } } // 文章阅读 func (c *BlogController) Index() { c.Prepare() c.TplName = "blog/index.tpl" blogId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) if blogId <= 0 { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.page_not_existed")) } blogReadSession := fmt.Sprintf("blog:read:%d", blogId) blog, err := models.NewBlog().FindFromCache(blogId) if err != nil { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.blog_not_existed")) } if c.Ctx.Input.IsPost() { password := c.GetString("password") if blog.BlogStatus == "password" && password != blog.Password { c.JsonResult(6001, i18n.Tr(c.Lang, "message.blog_pwd_incorrect")) } else if blog.BlogStatus == "password" && password == blog.Password { // Store the session value for the next GET request. _ = c.CruSession.Set(context.TODO(), blogReadSession, blogId) c.JsonResult(0, "OK") } else { c.JsonResult(0, "OK") } } else if blog.BlogStatus == "password" && c.CruSession.Get(context.TODO(), blogReadSession) == nil && // Read session doesn't exist (c.Member == nil || (blog.MemberId != c.Member.MemberId && !c.Member.IsAdministrator())) { // User isn't author or administrator //如果不存在已输入密码的标记 c.TplName = "blog/index_password.tpl" } if blog.BlogType != 1 { //加载文章附件 _ = blog.LinkAttach() } c.Data["Model"] = blog c.Data["Content"] = template.HTML(blog.BlogRelease) if blog.BlogExcerpt == "" { c.Data["Description"] = utils.AutoSummary(blog.BlogRelease, 120) } else { c.Data["Description"] = blog.BlogExcerpt } if nextBlog, err := models.NewBlog().QueryNext(blogId); err == nil { c.Data["Next"] = nextBlog } if preBlog, err := models.NewBlog().QueryPrevious(blogId); err == nil { c.Data["Previous"] = preBlog } } // 文章列表 func (c *BlogController) List() { c.Prepare() c.TplName = "blog/list.tpl" pageIndex, _ := c.GetInt("page", 1) var blogList []*models.Blog var totalCount int var err error blogList, totalCount, err = models.NewBlog().FindToPager(pageIndex, conf.PageSize, 0, "") if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() for _, blog := range blogList { //如果没有添加文章摘要,则自动提取 if blog.BlogExcerpt == "" { blog.BlogExcerpt = utils.AutoSummary(blog.BlogRelease, 120) } blog.Link() } } else { c.Data["PageHtml"] = "" } c.Data["Lists"] = blogList } // 管理后台文章列表 func (c *BlogController) ManageList() { c.Prepare() c.TplName = "blog/manage_list.tpl" pageIndex, _ := c.GetInt("page", 1) blogList, totalCount, err := models.NewBlog().FindToPager(pageIndex, conf.PageSize, c.Member.MemberId, "all") if err != nil { c.ShowErrorPage(500, err.Error()) } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["ModelList"] = blogList } // 文章设置 func (c *BlogController) ManageSetting() { c.Prepare() c.TplName = "blog/manage_setting.tpl" //如果是post请求 if c.Ctx.Input.IsPost() { blogId, _ := c.GetInt("id", 0) blogTitle := c.GetString("title") blogIdentify := c.GetString("identify") orderIndex, _ := c.GetInt("order_index", 0) blogType, _ := c.GetInt("blog_type", 0) blogExcerpt := c.GetString("excerpt", "") blogStatus := c.GetString("status", "publish") blogPassword := c.GetString("password", "") documentIdentify := strings.TrimSpace(c.GetString("documentIdentify")) bookIdentify := strings.TrimSpace(c.GetString("bookIdentify")) documentId := 0 if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } if blogTitle == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.blog_title_empty")) } if strings.Count(blogExcerpt, "") > 500 { c.JsonResult(6008, i18n.Tr(c.Lang, "message.blog_digest_tips")) } if blogStatus != "private" && blogStatus != "public" && blogStatus != "password" && blogStatus != "draft" { blogStatus = "public" } if blogStatus == "password" && blogPassword == "" { c.JsonResult(6010, i18n.Tr(c.Lang, "message.set_pwd_pls")) } if blogType != 0 && blogType != 1 { c.JsonResult(6005, i18n.Tr(c.Lang, "message.unknown_blog_type")) } if strings.Count(blogTitle, "") > 200 { c.JsonResult(6002, i18n.Tr(c.Lang, "message.blog_title_tips")) } //如果是关联文章,需要同步关联的文档 if blogType == 1 { book, err := models.NewBook().FindByIdentify(bookIdentify) if err != nil { c.JsonResult(6011, i18n.Tr(c.Lang, "message.ref_doc_not_exist_or_no_permit")) } doc, err := models.NewDocument().FindByIdentityFirst(documentIdentify, book.BookId) if err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.query_failed")) } documentId = doc.DocumentId // 如果不是超级管理员,则校验权限 if !c.Member.IsAdministrator() { bookResult, err := models.NewBookResult().FindByIdentify(book.Identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { c.JsonResult(6002, i18n.Tr(c.Lang, "message.ref_doc_not_exist_or_no_permit")) } } } var blog *models.Blog var err error //如果文章ID存在,则从数据库中查询文章 if blogId > 0 { if c.Member.IsAdministrator() { blog, err = models.NewBlog().Find(blogId) } else { blog, err = models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) } if err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.blog_not_exist")) } //如果设置了文章标识 if blogIdentify != "" { //如果查询到的文章标识存在并且不是当前文章的id if b, err := models.NewBlog().FindByIdentify(blogIdentify); err == nil && b.BlogId != blogId { c.JsonResult(6004, i18n.Tr(c.Lang, "message.blog_id_existed")) } } blog.Modified = time.Now() blog.ModifyAt = c.Member.MemberId } else { //如果设置了文章标识 if blogIdentify != "" { if models.NewBlog().IsExist(blogIdentify) { c.JsonResult(6004, i18n.Tr(c.Lang, "message.blog_id_existed")) } } blog = models.NewBlog() blog.MemberId = c.Member.MemberId blog.Created = time.Now() } if blogIdentify == "" { blog.BlogIdentify = fmt.Sprintf("%s-%d", "post", time.Now().UnixNano()) } else { blog.BlogIdentify = blogIdentify } blog.BlogTitle = blogTitle blog.OrderIndex = orderIndex blog.BlogType = blogType if blogType == 1 { blog.DocumentId = documentId } blog.BlogExcerpt = blogExcerpt blog.BlogStatus = blogStatus blog.Password = blogPassword if err := blog.Save(); err != nil { logs.Error("保存文章失败 -> ", err) c.JsonResult(6011, i18n.Tr(c.Lang, "message.failed")) } else { c.JsonResult(0, "ok", blog) } } if c.Ctx.Input.Referer() == "" { c.Data["Referer"] = "javascript:history.back();" } else { c.Data["Referer"] = c.Ctx.Input.Referer() } blogId, err := strconv.Atoi(c.Ctx.Input.Param(":id")) c.Data["DocumentIdentify"] = "" if err == nil { blog, err := models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) if err != nil { c.ShowErrorPage(500, err.Error()) } c.Data["Model"] = blog } else { c.Data["Model"] = models.NewBlog() } } // 文章创建或编辑 func (c *BlogController) ManageEdit() { c.Prepare() c.TplName = "blog/manage_edit.tpl" if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } if c.Ctx.Input.IsPost() { blogId, _ := c.GetInt("blogId", 0) if blogId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } blogContent := c.GetString("content", "") blogHtml := c.GetString("htmlContent", "") version, _ := c.GetInt64("version", 0) cover := c.GetString("cover") var blog *models.Blog var err error if c.Member.IsAdministrator() { blog, err = models.NewBlog().Find(blogId) } else { blog, err = models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) } if err != nil { logs.Error("查询文章失败 ->", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.query_failed")) } if version > 0 && blog.Version != version && cover != "yes" { c.JsonResult(6005, i18n.Tr(c.Lang, "message.blog_has_modified")) } //如果是关联文章,需要同步关联的文档 if blog.BlogType == 1 { doc, err := models.NewDocument().Find(blog.DocumentId) if err != nil { logs.Error("查询关联项目文档时出错 ->", err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.query_failed")) } book, err := models.NewBook().Find(doc.BookId) if err != nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } // 如果不是超级管理员,则校验权限 if !c.Member.IsAdministrator() { bookResult, err := models.NewBookResult().FindByIdentify(book.Identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.ref_doc_not_exist_or_no_permit")) } } doc.Markdown = blogContent doc.Release = blogHtml doc.Content = blogHtml doc.ModifyTime = time.Now() doc.ModifyAt = c.Member.MemberId if err := doc.InsertOrUpdate("markdown", "release", "content", "modify_time", "modify_at"); err != nil { logs.Error("保存关联文档时出错 ->", err) c.JsonResult(6004, i18n.Tr(c.Lang, "message.failed")) } } blog.BlogContent = blogContent blog.BlogRelease = blogHtml blog.ModifyAt = c.Member.MemberId blog.Modified = time.Now() if err := blog.Save("blog_content", "blog_release", "modify_at", "modify_time", "version"); err != nil { logs.Error("保存文章失败 -> ", err) c.JsonResult(6011, i18n.Tr(c.Lang, "message.failed")) } else { c.JsonResult(0, "ok", blog) } } blogId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) if blogId <= 0 { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.param_error")) } var blog *models.Blog var err error if c.Member.IsAdministrator() { blog, err = models.NewBlog().Find(blogId) } else { blog, err = models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) } if err != nil { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.blog_not_exist")) } blog.LinkAttach() if len(blog.AttachList) > 0 { returnJSON, err := json.Marshal(blog.AttachList) if err != nil { logs.Error("序列化文章附件时出错 ->", err) } else { c.Data["AttachList"] = template.JS(string(returnJSON)) } } else { c.Data["AttachList"] = template.JS("[]") } if conf.GetUploadFileSize() > 0 { c.Data["UploadFileSize"] = conf.GetUploadFileSize() } else { c.Data["UploadFileSize"] = "undefined" } c.Data["Model"] = blog } // 删除文章 func (c *BlogController) ManageDelete() { c.Prepare() blogId, _ := c.GetInt("blog_id", 0) if blogId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } var blog *models.Blog var err error if c.Member.IsAdministrator() { blog, err = models.NewBlog().Find(blogId) } else { blog, err = models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) } if err != nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.blog_not_exist")) } if err := blog.Delete(blogId); err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } else { c.JsonResult(0, i18n.Tr(c.Lang, "message.success")) } } // 上传附件或图片 func (c *BlogController) Upload() { c.Prepare() blogId, _ := c.GetInt("blogId") if blogId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } blog, err := models.NewBlog().Find(blogId) if err != nil { c.JsonResult(6010, i18n.Tr(c.Lang, "message.blog_not_exist")) } if !c.Member.IsAdministrator() && blog.MemberId != c.Member.MemberId { c.JsonResult(6011, i18n.Tr(c.Lang, "message.no_permission")) } name := "editormd-file-file" file, moreFile, err := c.GetFile(name) if err == http.ErrMissingFile { name = "editormd-image-file" file, moreFile, err = c.GetFile(name) if err == http.ErrMissingFile { c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty")) } } if err != nil { c.JsonResult(6002, err.Error()) } defer file.Close() type Size interface { Size() int64 } if conf.GetUploadFileSize() > 0 && moreFile.Size > conf.GetUploadFileSize() { c.JsonResult(6009, i18n.Tr(c.Lang, "message.upload_file_size_limit")) } ext := filepath.Ext(moreFile.Filename) if ext == "" { c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_type_error")) } //如果文件类型设置为 * 标识不限制文件类型 if web.AppConfig.DefaultString("upload_file_ext", "") != "*" { if !conf.IsAllowUploadFileExt(ext) { c.JsonResult(6004, i18n.Tr(c.Lang, "message.upload_file_type_error")) } } // 如果是超级管理员,则不判断权限 if c.Member.IsAdministrator() { _, err := models.NewBlog().Find(blogId) if err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.doc_not_exist_or_no_permit")) } } else { _, err := models.NewBlog().FindByIdAndMemberId(blogId, c.Member.MemberId) if err != nil { logs.Error("查询文章时出错 -> ", err) if err == orm.ErrNoRows { c.JsonResult(6006, i18n.Tr(c.Lang, "message.no_permission")) } c.JsonResult(6001, err.Error()) } } fileName := "attach_" + strconv.FormatInt(time.Now().UnixNano(), 16) filePath := filepath.Join(conf.WorkingDirectory, "uploads", "blog", time.Now().Format("200601"), fileName+ext) path := filepath.Dir(filePath) os.MkdirAll(path, os.ModePerm) err = c.SaveToFile(name, filePath) if err != nil { logs.Error("SaveToFile => ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } var httpPath string result := make(map[string]interface{}) //如果是图片,则当做内置图片处理,否则当做附件处理 if strings.EqualFold(ext, ".jpg") || strings.EqualFold(ext, ".jpeg") || strings.EqualFold(ext, ".png") || strings.EqualFold(ext, ".gif") { httpPath = "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1) if strings.HasPrefix(httpPath, "//") { httpPath = conf.URLForWithCdnImage(string(httpPath[1:])) } } else { attachment := models.NewAttachment() attachment.BookId = 0 attachment.FileName = moreFile.Filename attachment.CreateAt = c.Member.MemberId attachment.FileExt = ext attachment.FilePath = strings.TrimPrefix(filePath, conf.WorkingDirectory) attachment.DocumentId = blogId //如果是关联文章,则将附件设置为关联文档的文档上 if blog.BlogType == 1 { attachment.BookId = blog.BookId attachment.DocumentId = blog.DocumentId } if fileInfo, err := os.Stat(filePath); err == nil { attachment.FileSize = float64(fileInfo.Size()) } attachment.HttpPath = httpPath if err := attachment.Insert(); err != nil { os.Remove(filePath) logs.Error("保存文件附件失败 -> ", err) c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed")) } if attachment.HttpPath == "" { attachment.HttpPath = conf.URLForNotHost("BlogController.Download", ":id", blogId, ":attach_id", attachment.AttachmentId) if err := attachment.Update(); err != nil { logs.Error("保存文件失败 -> ", attachment.FilePath, err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } } result["attach"] = attachment } result["errcode"] = 0 result["success"] = 1 result["message"] = "ok" result["url"] = httpPath result["alt"] = fileName c.Ctx.Output.JSON(result, true, false) c.StopRun() } // 删除附件 func (c *BlogController) RemoveAttachment() { c.Prepare() attachId, _ := c.GetInt("attach_id") blogId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) if attachId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } blog, err := models.NewBlog().Find(blogId) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.doc_not_exist")) } else { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.query_failed")) } } attach, err := models.NewAttachment().Find(attachId) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.attachment_not_exist")) } if !c.Member.IsAdministrator() { _, err := models.NewBlog().FindByIdAndMemberId(attach.DocumentId, c.Member.MemberId) if err != nil { logs.Error(err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.doc_not_exist")) } } if blog.BlogType == 1 && attach.BookId != blog.BookId && attach.DocumentId != blog.DocumentId { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } else if attach.BookId != 0 || attach.DocumentId != blogId { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } if err := attach.Delete(); err != nil { logs.Error(err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } os.Remove(filepath.Join(conf.WorkingDirectory, attach.FilePath)) c.JsonResult(0, "ok", attach) } // 下载附件 func (c *BlogController) Download() { c.Prepare() blogId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) attachId, _ := strconv.Atoi(c.Ctx.Input.Param(":attach_id")) password := c.GetString("password") blog, err := models.NewBlog().Find(blogId) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.doc_not_exist")) } else { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.query_failed")) } } blogReadSession := fmt.Sprintf("blog:read:%d", blogId) //如果没有启动匿名访问,或者设置了访问密码 if (c.Member == nil && !c.EnableAnonymous) || (blog.BlogStatus == "password" && password != blog.Password && c.CruSession.Get(context.TODO(), blogReadSession) == nil) { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) } // 查找附件 attachment, err := models.NewAttachment().Find(attachId) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } else { logs.Error("查询附件时出现异常 -> ", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.query_failed")) } } //如果是链接的文章,需要校验文档ID是否一致,如果不是,需要保证附件的项目ID为0且文档的ID等于博文ID if blog.BlogType == 1 && attachment.DocumentId != blog.DocumentId { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } else if blog.BlogType != 1 && (attachment.BookId != 0 || attachment.DocumentId != blogId) { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } c.Ctx.Output.Download(filepath.Join(conf.WorkingDirectory, attachment.FilePath), attachment.FileName) c.StopRun() } ================================================ FILE: controllers/BookController.go ================================================ package controllers import ( "context" "encoding/json" "errors" "fmt" "html/template" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/utils/sqltil" "net/http" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/graphics" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/pagination" "github.com/russross/blackfriday/v2" ) type BookController struct { BaseController } func (c *BookController) Index() { c.Prepare() c.TplName = "book/index.tpl" pageIndex, _ := c.GetInt("page", 1) books, totalCount, err := models.NewBook().FindToPager(pageIndex, conf.PageSize, c.Member.MemberId, c.Lang) if err != nil { logs.Error("BookController.Index => ", err) c.Abort("500") } for i, book := range books { books[i].Description = utils.StripTags(string(blackfriday.Run([]byte(book.Description)))) books[i].ModifyTime = book.ModifyTime.Local() books[i].CreateTime = book.CreateTime.Local() } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(books) if err != nil || len(books) <= 0 { c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } if itemsets, err := models.NewItemsets().First(1); err == nil { c.Data["Item"] = itemsets } } // Dashboard 项目概要 . func (c *BookController) Dashboard() { c.Prepare() c.TplName = "book/dashboard.tpl" key := c.Ctx.Input.Param(":key") if key == "" { c.Abort("404") } book, err := models.NewBookResult().SetLang(c.Lang).FindByIdentify(key, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.Abort("403") } c.Abort("500") return } c.Data["Description"] = template.HTML(blackfriday.Run([]byte(book.Description))) c.Data["Model"] = *book } // Setting 项目设置 . func (c *BookController) Setting() { c.Prepare() c.TplName = "book/setting.tpl" key := c.Ctx.Input.Param(":key") if key == "" { c.Abort("404") } book, err := models.NewBookResult().FindByIdentify(key, c.Member.MemberId) if err != nil { if err == orm.ErrNoRows { c.Abort("404") } if err == models.ErrPermissionDenied { c.Abort("403") } c.Abort("500") return } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.Abort("403") } if book.PrivateToken != "" { book.PrivateToken = conf.URLFor("DocumentController.Index", ":key", book.Identify, "token", book.PrivateToken) } c.Data["Model"] = book } // 保存项目信息 func (c *BookController) SaveBook() { bookResult, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) } book, err := models.NewBook().Find(bookResult.BookId) if err != nil { logs.Error("SaveBook => ", err) c.JsonResult(6002, err.Error()) } bookName := strings.TrimSpace(c.GetString("book_name")) description := strings.TrimSpace(c.GetString("description", "")) commentStatus := c.GetString("comment_status") //tag := strings.TrimSpace(c.GetString("label")) editor := strings.TrimSpace(c.GetString("editor")) autoRelease := strings.TrimSpace(c.GetString("auto_release")) == "on" publisher := strings.TrimSpace(c.GetString("publisher")) historyCount, _ := c.GetInt("history_count", 0) isDownload := strings.TrimSpace(c.GetString("is_download")) == "on" enableShare := strings.TrimSpace(c.GetString("enable_share")) == "on" isUseFirstDocument := strings.TrimSpace(c.GetString("is_use_first_document")) == "on" autoSave := strings.TrimSpace(c.GetString("auto_save")) == "on" itemId, _ := c.GetInt("itemId") pringState := strings.TrimSpace(c.GetString("print_state")) == "on" if strings.Count(description, "") > 500 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.project_desc_tips")) } if commentStatus != "open" && commentStatus != "closed" && commentStatus != "group_only" && commentStatus != "registered_only" { commentStatus = "closed" } if !models.NewItemsets().Exist(itemId) { c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_space_not_exist")) } // if editor != EditorMarkdown && editor != EditorCherryMarkdown && editor != EditorHtml && editor != EditorNewHtml { if editor != EditorMarkdown && editor != EditorCherryMarkdown && editor != EditorHtml && editor != EditorNewHtml && editor != EditorFroala { editor = EditorMarkdown } book.BookName = bookName book.Description = description book.CommentStatus = commentStatus book.Publisher = publisher //book.Label = tag if book.Editor == EditorMarkdown && editor == EditorCherryMarkdown || book.Editor == EditorCherryMarkdown && editor == EditorMarkdown { c.JsonResult(6006, i18n.Tr(c.Lang, "message.editors_not_compatible")) } book.Editor = editor if editor == EditorCherryMarkdown { book.Theme = "cherry" } book.HistoryCount = historyCount book.IsDownload = 0 book.BookPassword = strings.TrimSpace(c.GetString("bPassword")) book.ItemId = itemId if autoRelease { book.AutoRelease = 1 } else { book.AutoRelease = 0 } if isDownload { book.IsDownload = 0 } else { book.IsDownload = 1 } if enableShare { book.IsEnableShare = 0 } else { book.IsEnableShare = 1 } if isUseFirstDocument { book.IsUseFirstDocument = 1 } else { book.IsUseFirstDocument = 0 } if autoSave { book.AutoSave = 1 } else { book.AutoSave = 0 } if pringState { book.PrintSate = 1 } else { book.PrintSate = 0 } if err := book.Update(); err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed")) } bookResult.BookName = bookName bookResult.Description = description bookResult.CommentStatus = commentStatus logs.Info("用户 [", c.Member.Account, "] 修改了项目 ->", book) c.JsonResult(0, "ok", bookResult) } // 设置项目私有状态. func (c *BookController) PrivatelyOwned() { status := c.GetString("status") if status != "open" && status != "close" { c.JsonResult(6003, i18n.Tr(c.Lang, "message.param_error")) } state := 0 if status == "open" { state = 0 } else { state = 1 } bookResult, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) return } //只有创始人才能变更私有状态 if bookResult.RoleId != conf.BookFounder { c.JsonResult(6002, i18n.Tr(c.Lang, "message.no_permission")) } book, err := models.NewBook().Find(bookResult.BookId) if err != nil { c.JsonResult(6005, i18n.Tr(c.Lang, "message.item_not_exist")) return } book.PrivatelyOwned = state err = book.Update() if err != nil { logs.Error("PrivatelyOwned => ", err) c.JsonResult(6004, i18n.Tr(c.Lang, "message.failed")) } logs.Info("用户 【", c.Member.Account, "]修改了项目权限 ->", state) c.JsonResult(0, "ok") } // Transfer 转让项目. func (c *BookController) Transfer() { c.Prepare() account := c.GetString("account") if account == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.receive_account_empty")) } member, err := models.NewMember().FindByAccount(account) if err != nil { logs.Error("FindByAccount => ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.receive_account_not_exist")) } if member.Status != 0 { c.JsonResult(6006, i18n.Tr(c.Lang, "message.receive_account_disabled")) } if member.MemberId == c.Member.MemberId { c.JsonResult(6007, i18n.Tr(c.Lang, "message.cannot_handover_myself")) } bookResult, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) return } err = models.NewRelationship().Transfer(bookResult.BookId, c.Member.MemberId, member.MemberId) if err != nil { logs.Error("转让项目失败 -> ", err) c.JsonResult(6008, err.Error()) } c.JsonResult(0, "ok") } // 上传项目封面. func (c *BookController) UploadCover() { bookResult, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) return } book, err := models.NewBook().Find(bookResult.BookId) if err != nil { logs.Error("SaveBook => ", err) c.JsonResult(6002, err.Error()) } file, moreFile, err := c.GetFile("image-file") if err != nil { logs.Error("获取上传文件失败 ->", err.Error()) c.JsonResult(500, "读取文件异常") return } defer file.Close() ext := filepath.Ext(moreFile.Filename) if !strings.EqualFold(ext, ".png") && !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".gif") && !strings.EqualFold(ext, ".jpeg") { c.JsonResult(500, "不支持的图片格式") } x1, _ := strconv.ParseFloat(c.GetString("x"), 10) y1, _ := strconv.ParseFloat(c.GetString("y"), 10) w1, _ := strconv.ParseFloat(c.GetString("width"), 10) h1, _ := strconv.ParseFloat(c.GetString("height"), 10) x := int(x1) y := int(y1) width := int(w1) height := int(h1) fileName := "cover_" + strconv.FormatInt(time.Now().UnixNano(), 16) //附件路径按照项目组织 // filePath := filepath.Join("uploads", book.Identify, "images", fileName+ext) filePath := filepath.Join(conf.WorkingDirectory, "uploads", book.Identify, "images", fileName+ext) path := filepath.Dir(filePath) os.MkdirAll(path, os.ModePerm) err = c.SaveToFile("image-file", filePath) if err != nil { logs.Error("", err) c.JsonResult(500, "图片保存失败") } defer func(filePath string) { os.Remove(filePath) }(filePath) //剪切图片 subImg, err := graphics.ImageCopyFromFile(filePath, x, y, width, height) if err != nil { logs.Error("graphics.ImageCopyFromFile => ", err) c.JsonResult(500, "图片剪切") } filePath = filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), fileName+"_small"+ext) //生成缩略图并保存到磁盘 err = graphics.ImageResizeSaveFile(subImg, 350, 460, filePath) if err != nil { logs.Error("ImageResizeSaveFile => ", err.Error()) c.JsonResult(500, "保存图片失败") } url := "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1) if strings.HasPrefix(url, "//") { url = string(url[1:]) } oldCover := book.Cover book.Cover = conf.URLForWithCdnImage(url) if err := book.Update(); err != nil { c.JsonResult(6001, "保存图片失败") } //如果原封面不是默认封面则删除 if oldCover != conf.GetDefaultCover() { os.Remove("." + oldCover) } logs.Info("用户[", c.Member.Account, "]上传了项目封面 ->", book.BookName, book.BookId, book.Cover) c.JsonResult(0, "ok", url) } // Users 用户列表. func (c *BookController) Users() { c.Prepare() c.TplName = "book/users.tpl" key := c.Ctx.Input.Param(":key") pageIndex, _ := c.GetInt("page", 1) if key == "" { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } book, err := models.NewBookResult().FindByIdentify(key, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.Abort("403") } c.Abort("500") return } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.Abort("403") } c.Data["Model"] = *book members, totalCount, err := models.NewMemberRelationshipResult().FindForUsersByBookId(c.Lang, book.BookId, pageIndex, conf.PageSize) if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(members) if err != nil { c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } // Create 创建项目. func (c *BookController) Create() { if c.Ctx.Input.IsPost() { bookName := strings.TrimSpace(c.GetString("book_name", "")) identify := strings.TrimSpace(c.GetString("identify", "")) description := strings.TrimSpace(c.GetString("description", "")) privatelyOwned, _ := strconv.Atoi(c.GetString("privately_owned")) commentStatus := c.GetString("comment_status") editor := c.GetString("editor") itemId, _ := c.GetInt("itemId") if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } if bookName == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.project_name_empty")) } if identify == "" { c.JsonResult(6002, i18n.Tr(c.Lang, "message.project_id_empty")) } if ok, err := regexp.MatchString(`^[a-z]+[a-zA-Z0-9_\-]*$`, identify); !ok || err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.project_id_tips")) } if strings.Count(identify, "") > 50 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.project_id_length")) } if strings.Count(description, "") > 500 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.project_desc_tips")) } if privatelyOwned != 0 && privatelyOwned != 1 { privatelyOwned = 1 } if !models.NewItemsets().Exist(itemId) { c.JsonResult(6005, i18n.Tr(c.Lang, "message.project_space_not_exist")) } if commentStatus != "open" && commentStatus != "closed" && commentStatus != "group_only" && commentStatus != "registered_only" { commentStatus = "closed" } book := models.NewBook() book.Cover = conf.GetDefaultCover() //如果客户端上传了项目封面则直接保存 if file, moreFile, err := c.GetFile("image-file"); err == nil { defer file.Close() ext := filepath.Ext(moreFile.Filename) //如果上传的是图片 if strings.EqualFold(ext, ".png") || strings.EqualFold(ext, ".jpg") || strings.EqualFold(ext, ".gif") || strings.EqualFold(ext, ".jpeg") { fileName := "cover_" + strconv.FormatInt(time.Now().UnixNano(), 16) filePath := filepath.Join("uploads", time.Now().Format("200601"), fileName+ext) path := filepath.Dir(filePath) os.MkdirAll(path, os.ModePerm) if err := c.SaveToFile("image-file", filePath); err == nil { url := "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1) if strings.HasPrefix(url, "//") { url = string(url[1:]) } book.Cover = url } } } if books, _ := book.FindByField("identify", identify, "book_id"); len(books) > 0 { c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_id_existed")) } book.BookName = bookName book.Description = description book.CommentCount = 0 book.PrivatelyOwned = privatelyOwned book.CommentStatus = commentStatus book.Identify = identify book.DocCount = 0 book.MemberId = c.Member.MemberId book.Version = time.Now().Unix() book.IsEnableShare = 0 book.IsUseFirstDocument = 1 book.IsDownload = 1 book.AutoRelease = 0 book.ItemId = itemId book.Editor = editor book.Theme = "default" if err := book.Insert(c.Lang); err != nil { logs.Error("Insert => ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } bookResult, err := models.NewBookResult().FindByIdentify(book.Identify, c.Member.MemberId) if err != nil { logs.Error(err) } logs.Info("用户[", c.Member.Account, "]创建了项目 ->", book) c.JsonResult(0, "ok", bookResult) } c.JsonResult(6001, "error") } // 复制项目 func (c *BookController) Copy() { if c.Ctx.Input.IsPost() { //检查是否有复制项目的权限 if _, err := c.IsPermission(); err != nil { c.JsonResult(500, err.Error()) } if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } identify := strings.TrimSpace(c.GetString("identify", "")) if identify == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } book := models.NewBook() err := book.Copy(identify) if err != nil { c.JsonResult(6002, "复制项目出错") } else { bookResult, err := models.NewBookResult().FindByIdentify(book.Identify, c.Member.MemberId) if err != nil { logs.Error("查询失败") } c.JsonResult(0, "ok", bookResult) } } } // 导入zip压缩包或docx func (c *BookController) Import() { if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } file, moreFile, err := c.GetFile("import-file") if err == http.ErrMissingFile { c.JsonResult(6003, "没有发现需要上传的文件") } defer file.Close() bookName := strings.TrimSpace(c.GetString("book_name")) identify := strings.TrimSpace(c.GetString("identify")) description := strings.TrimSpace(c.GetString("description", "")) privatelyOwned, _ := strconv.Atoi(c.GetString("privately_owned")) itemId, _ := c.GetInt("itemId") if bookName == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.project_name_empty")) } if len([]rune(bookName)) > 500 { c.JsonResult(6002, "项目名称不能大于500字") } if identify == "" { c.JsonResult(6002, i18n.Tr(c.Lang, "message.project_id_empty")) } if ok, err := regexp.MatchString(`^[a-z]+[a-zA-Z0-9_\-]*$`, identify); !ok || err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.project_id_tips")) } if !models.NewItemsets().Exist(itemId) { c.JsonResult(6007, i18n.Tr(c.Lang, "message.project_space_not_exist")) } if strings.Count(identify, "") > 50 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.project_id_length")) } ext := filepath.Ext(moreFile.Filename) if !strings.EqualFold(ext, ".zip") && !strings.EqualFold(ext, ".docx") { c.JsonResult(6004, "不支持的文件类型") } if books, _ := models.NewBook().FindByField("identify", identify, "book_id"); len(books) > 0 { c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_id_existed")) } tempPath := filepath.Join(os.TempDir(), c.CruSession.SessionID(context.TODO())) os.MkdirAll(tempPath, 0766) tempPath = filepath.Join(tempPath, moreFile.Filename) err = c.SaveToFile("import-file", tempPath) if err != nil { c.JsonResult(6004, i18n.Tr(c.Lang, "message.upload_failed")) } book := models.NewBook() book.MemberId = c.Member.MemberId book.Cover = conf.GetDefaultCover() book.BookName = bookName book.Description = description book.CommentCount = 0 book.PrivatelyOwned = privatelyOwned book.CommentStatus = "closed" book.Identify = identify book.DocCount = 0 book.MemberId = c.Member.MemberId book.Version = time.Now().Unix() book.ItemId = itemId book.Editor = "markdown" book.Theme = "default" if strings.EqualFold(ext, ".zip") { go book.ImportBook(tempPath, c.Lang) } else if strings.EqualFold(ext, ".docx") { go book.ImportWordBook(tempPath, c.Lang) } logs.Info("用户[", c.Member.Account, "]导入了项目 ->", book) c.JsonResult(0, "项目正在后台转换中,请稍后查看") } // CreateToken 创建访问来令牌. //func (c *BookController) CreateToken() { // // action := c.GetString("action") // // bookResult, err := c.IsPermission() // // if err != nil { // if err == models.ErrPermissionDenied { // c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) // } // if err == orm.ErrNoRows { // c.JsonResult(404, i18n.Tr(c.Lang, "message.item_not_exist")) // } // logs.Error("生成阅读令牌失败 =>", err) // c.JsonResult(6002, err.Error()) // } // book := models.NewBook() // // if _, err := book.Find(bookResult.BookId); err != nil { // c.JsonResult(6001, i18n.Tr(c.Lang, "message.item_not_exist")) // } // if action == "create" { // if bookResult.PrivatelyOwned == 0 { // c.JsonResult(6001, "公开项目不能创建阅读令牌") // } // // book.PrivateToken = string(utils.Krand(conf.GetTokenSize(), utils.KC_RAND_KIND_ALL)) // if err := book.Update(); err != nil { // logs.Error("生成阅读令牌失败 => ", err) // c.JsonResult(6003, "生成阅读令牌失败") // } // logs.Info("用户[", c.Member.Account, "]创建项目令牌 ->", book.PrivateToken) // c.JsonResult(0, "ok", conf.URLFor("DocumentController.Index", ":key", book.Identify, "token", book.PrivateToken)) // } else { // book.PrivateToken = "" // if err := book.Update(); err != nil { // logs.Error("CreateToken => ", err) // c.JsonResult(6004, "删除令牌失败") // } // logs.Info("用户[", c.Member.Account, "]创建项目令牌 ->", book.PrivateToken) // c.JsonResult(0, "ok", "") // } //} // Delete 删除项目. func (c *BookController) Delete() { c.Prepare() bookResult, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) return } if bookResult.RoleId != conf.BookFounder { c.JsonResult(6002, "只有创始人才能删除项目") } err = models.NewBook().ThoroughDeleteBook(bookResult.BookId) if err == orm.ErrNoRows { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist")) } if err != nil { logs.Error("删除项目 => ", err) c.JsonResult(6003, "删除失败") } logs.Info("用户[", c.Member.Account, "]删除了项目 ->", bookResult) c.JsonResult(0, "ok") } // 发布项目. func (c *BookController) Release() { c.Prepare() identify := c.GetString("identify") bookId := 0 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("发布文档失败 ->", err) c.JsonResult(6003, "文档不存在") return } bookId = book.BookId } else { book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } if err == orm.ErrNoRows { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist")) } logs.Error(err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.unknown_exception")) } if book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder && book.RoleId != conf.BookEditor { c.JsonResult(6003, i18n.Tr(c.Lang, "message.no_permission")) } bookId = book.BookId } go models.NewBook().ReleaseContent(bookId, c.Lang) c.JsonResult(0, i18n.Tr(c.Lang, "message.publish_to_queue")) } // 更新项目排序 func (c *BookController) UpdateBookOrder() { if !c.Member.IsAdministrator() { c.JsonResult(403, "权限不足") return } type Params struct { Ids string `form:"ids"` } var params Params if err := c.ParseForm(¶ms); err != nil { c.JsonResult(6003, "参数错误") return } idArray := strings.Split(params.Ids, ",") orderCount := len(idArray) for _, id := range idArray { bookId, _ := strconv.Atoi(id) orderCount-- book, err := models.NewBook().Find(bookId) if err != nil { continue } book.BookId = bookId book.OrderIndex = orderCount err = book.Update() if err != nil { continue } } c.JsonResult(0, "ok") } // 文档排序. func (c *BookController) SaveSort() { c.Prepare() identify := c.Ctx.Input.Param(":key") if identify == "" { c.Abort("404") } bookId := 0 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil || book == nil { c.JsonResult(6001, i18n.Tr(c.Lang, "message.item_not_exist")) return } bookId = book.BookId } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { logs.Error("DocumentController.Edit => ", err) c.Abort("403") } if bookResult.RoleId == conf.BookObserver { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = bookResult.BookId } content := c.Ctx.Input.RequestBody var docs []map[string]interface{} err := json.Unmarshal(content, &docs) if err != nil { logs.Error(err) c.JsonResult(6003, "数据错误") } for _, item := range docs { if docId, ok := item["id"].(float64); ok { doc, err := models.NewDocument().Find(int(docId)) if err != nil { logs.Error(err) continue } if doc.BookId != bookId { logs.Info("%s", i18n.Tr(c.Lang, "message.no_permission")) continue } sort, ok := item["sort"].(float64) if !ok { logs.Info("排序数字转换失败 => ", item) continue } parentId, ok := item["parent"].(float64) if !ok { logs.Info("父分类转换失败 => ", item) continue } if parentId > 0 { if parent, err := models.NewDocument().Find(int(parentId)); err != nil || parent.BookId != bookId { continue } } doc.OrderSort = int(sort) doc.ParentId = int(parentId) if err := doc.InsertOrUpdate(); err != nil { fmt.Printf("%s", err.Error()) logs.Error(err) } } else { fmt.Printf("文档ID转换失败 => %+v", item) } } c.JsonResult(0, "ok") } func (c *BookController) Team() { c.Prepare() c.TplName = "book/team.tpl" key := c.Ctx.Input.Param(":key") pageIndex, _ := c.GetInt("page", 1) if key == "" { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } book, err := models.NewBookResult().FindByIdentify(key, c.Member.MemberId) if err != nil || book == nil { if err == models.ErrPermissionDenied { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) } c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) return } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.Abort("403") } c.Data["Model"] = book members, totalCount, err := models.NewTeamRelationship().FindByBookToPager(book.BookId, pageIndex, conf.PageSize) if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(members) if err != nil { c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } func (c *BookController) TeamAdd() { c.Prepare() teamId, _ := c.GetInt("teamId") book, err := c.IsPermission() if err != nil { c.JsonResult(500, err.Error()) return } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.Abort("403") } _, err = models.NewTeam().First(teamId, "team_id") if err != nil { if err == orm.ErrNoRows { c.JsonResult(500, "团队不存在") } c.JsonResult(5002, err.Error()) } if _, err := models.NewTeamRelationship().FindByBookId(book.BookId, teamId); err == nil { c.JsonResult(5003, "团队已加入当前项目") } teamRel := models.NewTeamRelationship() teamRel.BookId = book.BookId teamRel.TeamId = teamId err = teamRel.Save() if err != nil { c.JsonResult(5004, "加入项目失败") return } teamRel.Include() c.JsonResult(0, "OK", teamRel) } // 删除项目的团队. func (c *BookController) TeamDelete() { c.Prepare() teamId, _ := c.GetInt("teamId") if teamId <= 0 { c.JsonResult(5001, i18n.Tr(c.Lang, "message.param_error")) } book, err := c.IsPermission() if err != nil { c.JsonResult(5002, err.Error()) return } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.Abort("403") } err = models.NewTeamRelationship().DeleteByBookId(book.BookId, teamId) if err != nil { if err == orm.ErrNoRows { c.JsonResult(5003, "团队未加入项目") } c.JsonResult(5004, err.Error()) } c.JsonResult(0, "OK") } // 团队搜索. func (c *BookController) TeamSearch() { c.Prepare() keyword := strings.TrimSpace(c.GetString("q")) book, err := c.IsPermission() if err != nil { c.JsonResult(500, err.Error()) } keyword = sqltil.EscapeLike(keyword) searchResult, err := models.NewTeamRelationship().FindNotJoinBookByBookIdentify(book.BookId, keyword, 10) if err != nil { c.JsonResult(500, err.Error(), searchResult) } c.JsonResult(0, "OK", searchResult) } // 项目空间搜索. func (c *BookController) ItemsetsSearch() { c.Prepare() keyword := strings.TrimSpace(c.GetString("q")) keyword = sqltil.EscapeLike(keyword) searchResult, err := models.NewItemsets().FindItemsetsByName(keyword, 10) if err != nil { c.JsonResult(500, err.Error(), searchResult) } c.JsonResult(0, "OK", searchResult) } func (c *BookController) IsPermission() (*models.BookResult, error) { identify := c.GetString("identify") if identify == "" { return nil, errors.New(i18n.Tr(c.Lang, "message.param_error")) } book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { return book, errors.New(i18n.Tr(c.Lang, "message.no_permission")) } if err == orm.ErrNoRows { return book, errors.New(i18n.Tr(c.Lang, "message.item_not_exist")) } return book, err } if book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder { return book, errors.New(i18n.Tr(c.Lang, "message.no_permission")) } return book, nil } ================================================ FILE: controllers/BookMemberController.go ================================================ package controllers import ( "errors" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" ) type BookMemberController struct { BaseController } // AddMember 参加参与用户. func (c *BookMemberController) AddMember() { identify := c.GetString("identify") account, _ := c.GetInt("account") roleId, _ := c.GetInt("role_id", 3) logs.Info(account) if identify == "" || account <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } book, err := c.IsPermission() if err != nil { c.JsonResult(6001, err.Error()) } member := models.NewMember() if _, err := member.Find(account); err != nil { c.JsonResult(404, i18n.Tr(c.Lang, "message.user_not_existed")) } if member.Status == 1 { c.JsonResult(6003, i18n.Tr(c.Lang, "message.user_disable")) } if _, err := models.NewRelationship().FindForRoleId(book.BookId, member.MemberId); err == nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.user_exist_in_proj")) } //如果是只读用户,只能设置为观察者 if member.Role == conf.MemberReaderRole && roleId != int(conf.BookObserver) { c.JsonResult(6003, i18n.Tr(c.Lang, "message.readusr_only_observer")) } relationship := models.NewRelationship() relationship.BookId = book.BookId relationship.MemberId = member.MemberId relationship.RoleId = conf.BookRole(roleId) if err := relationship.Insert(); err == nil { memberRelationshipResult := models.NewMemberRelationshipResult().FromMember(member) memberRelationshipResult.RoleId = conf.BookRole(roleId) memberRelationshipResult.RelationshipId = relationship.RelationshipId memberRelationshipResult.BookId = book.BookId memberRelationshipResult.ResolveRoleName(c.Lang) c.JsonResult(0, "ok", memberRelationshipResult) } c.JsonResult(500, err.Error()) } // 变更指定用户在指定项目中的权限 func (c *BookMemberController) ChangeRole() { identify := c.GetString("identify") memberId, _ := c.GetInt("member_id", 0) role, _ := c.GetInt("role_id", 0) if identify == "" || memberId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if memberId == c.Member.MemberId { c.JsonResult(6006, i18n.Tr(c.Lang, "message.cannot_change_own_priv")) } book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) } if err == orm.ErrNoRows { c.JsonResult(404, i18n.Tr(c.Lang, "message.item_not_exist")) } c.JsonResult(6002, err.Error()) } if book.RoleId != 0 && book.RoleId != 1 { c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) } member := models.NewMember() if _, err := member.Find(memberId); err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.user_not_existed")) } if member.Status == 1 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.user_disable")) } //如果是只读用户,只能设置为观察者 if member.Role == conf.MemberReaderRole && role != int(conf.BookObserver) { c.JsonResult(6003, i18n.Tr(c.Lang, "message.readusr_only_observer")) } relationship, err := models.NewRelationship().UpdateRoleId(book.BookId, memberId, conf.BookRole(role)) if err != nil { logs.Error("变更用户在项目中的权限 => ", err) c.JsonResult(6005, err.Error()) } memberRelationshipResult := models.NewMemberRelationshipResult().FromMember(member) memberRelationshipResult.RoleId = relationship.RoleId memberRelationshipResult.RelationshipId = relationship.RelationshipId memberRelationshipResult.BookId = book.BookId memberRelationshipResult.ResolveRoleName(c.Lang) c.JsonResult(0, "ok", memberRelationshipResult) } // 删除参与者. func (c *BookMemberController) RemoveMember() { identify := c.GetString("identify") member_id, _ := c.GetInt("member_id", 0) if identify == "" || member_id <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if member_id == c.Member.MemberId { c.JsonResult(6006, i18n.Tr(c.Lang, "message.cannot_delete_self")) } book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) } if err == orm.ErrNoRows { c.JsonResult(404, i18n.Tr(c.Lang, "message.item_not_exist")) } c.JsonResult(6002, err.Error()) } //如果不是创始人也不是管理员则不能操作 if book.RoleId != conf.BookFounder && book.RoleId != conf.BookAdmin { c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) } err = models.NewRelationship().DeleteByBookIdAndMemberId(book.BookId, member_id) if err != nil { c.JsonResult(6007, err.Error()) } c.JsonResult(0, "ok") } func (c *BookMemberController) IsPermission() (*models.BookResult, error) { identify := c.GetString("identify") book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { return book, errors.New(i18n.Tr(c.Lang, "message.no_permission")) } if err == orm.ErrNoRows { return book, errors.New(i18n.Tr(c.Lang, "message.item_not_exist")) } return book, err } if book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder { return book, errors.New(i18n.Tr(c.Lang, "message.no_permission")) } return book, nil } ================================================ FILE: controllers/CommentController.go ================================================ package controllers import ( "strings" "time" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils/pagination" ) type CommentController struct { BaseController } func (c *CommentController) Lists() { docid, _ := c.GetInt("docid", 0) pageIndex, _ := c.GetInt("page", 1) // 获取评论、分页 comments, count, pageIndex := models.NewComment().QueryCommentByDocumentId(docid, pageIndex, conf.PageSize, c.Member) page := pagination.PageUtil(int(count), pageIndex, conf.PageSize, comments) var data struct { DocId int `json:"doc_id"` Page pagination.Page `json:"page"` } data.DocId = docid data.Page = page c.JsonResult(0, "ok", data) return } func (c *CommentController) Create() { content := c.GetString("content") id, _ := c.GetInt("doc_id") _, err := models.NewDocument().Find(id) if err != nil { c.JsonResult(1, "文章不存在") } m := models.NewComment() m.DocumentId = id if c.Member == nil { c.JsonResult(1, "请先登录,再评论") } if len(c.Member.RealName) != 0 { m.Author = c.Member.RealName } else { m.Author = c.Member.Account } m.MemberId = c.Member.MemberId m.IPAddress = c.Ctx.Request.RemoteAddr m.IPAddress = strings.Split(m.IPAddress, ":")[0] m.CommentDate = time.Now() m.Content = content m.Insert() var data struct { DocId int `json:"doc_id"` } data.DocId = id c.JsonResult(0, "ok", data) } func (c *CommentController) Index() { c.Prepare() c.TplName = "comment/index.tpl" } func (c *CommentController) Delete() { if c.Ctx.Input.IsPost() { id, _ := c.GetInt("id", 0) m, err := models.NewComment().Find(id) if err != nil { c.JsonResult(1, "评论不存在") } doc, err := models.NewDocument().Find(m.DocumentId) if err != nil { c.JsonResult(1, "文章不存在") } // 判断是否有权限删除 bookRole, _ := models.NewRelationship().FindForRoleId(doc.BookId, c.Member.MemberId) if m.CanDelete(c.Member.MemberId, bookRole) { err := m.Delete() if err != nil { c.JsonResult(1, "删除错误") } else { c.JsonResult(0, "ok") } } else { c.JsonResult(1, "没有权限删除") } } } ================================================ FILE: controllers/DocumentController.go ================================================ package controllers import ( "context" "encoding/json" "fmt" "html/template" "image/png" "io" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/boombuler/barcode" "github.com/boombuler/barcode/qr" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/cryptil" "github.com/mindoc-org/mindoc/utils/filetil" "github.com/mindoc-org/mindoc/utils/gopool" "github.com/mindoc-org/mindoc/utils/pagination" "github.com/russross/blackfriday/v2" ) // DocumentController struct type DocumentController struct { BaseController } // Document prev&next type DocumentTreeFlatten struct { DocumentId int `json:"id"` DocumentName string `json:"text"` // ParentId interface{} `json:"parent"` Identify string `json:"identify"` // BookIdentify string `json:"-"` // Version int64 `json:"version"` } // 文档首页 func (c *DocumentController) Index() { c.Prepare() identify := c.Ctx.Input.Param(":key") token := c.GetString("token") if identify == "" { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } // 如果没有开启匿名访问则跳转到登录 if !c.EnableAnonymous && !c.isUserLoggedIn() { promptUserToLogIn(c) return } bookResult := c.isReadable(identify, token) c.TplName = "document/" + bookResult.Theme + "_read.tpl" selected := 0 if bookResult.IsUseFirstDocument { doc, err := bookResult.FindFirstDocumentByBookId(bookResult.BookId) if err == nil { selected = doc.DocumentId c.Data["Title"] = doc.DocumentName c.Data["Content"] = template.HTML(doc.Release) c.Data["Description"] = utils.AutoSummary(doc.Release, 120) c.Data["FoldSetting"] = "first" if bookResult.Editor == EditorCherryMarkdown { c.Data["MarkdownTheme"] = doc.MarkdownTheme } if bookResult.IsDisplayComment { // 获取评论、分页 comments, count, _ := models.NewComment().QueryCommentByDocumentId(doc.DocumentId, 1, conf.PageSize, c.Member) page := pagination.PageUtil(int(count), 1, conf.PageSize, comments) c.Data["Page"] = page } } } else { c.Data["Title"] = i18n.Tr(c.Lang, "blog.summary") c.Data["Content"] = template.HTML(blackfriday.Run([]byte(bookResult.Description))) c.Data["FoldSetting"] = "closed" } tree, err := models.NewDocument().CreateDocumentTreeForHtml(bookResult.BookId, selected) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.no_doc_in_cur_proj")) } else { logs.Error("生成项目文档树时出错 -> ", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.build_doc_tree_error")) } } c.Data["IS_DOCUMENT_INDEX"] = true c.Data["Model"] = bookResult c.Data["Result"] = template.HTML(tree) } // CheckPassword : Handles password verification for private documents, // and front-end requests are made through Ajax. func (c *DocumentController) CheckPassword() { identify := c.Ctx.Input.Param(":key") password := c.GetString("bPassword") if identify == "" || password == "" { c.JsonResult(http.StatusBadRequest, i18n.Tr(c.Lang, "message.param_error")) } // You have not logged in and need to log in again. if !c.EnableAnonymous && !c.isUserLoggedIn() { logs.Info("You have not logged in and need to log in again(SessionId: %s).", c.CruSession.SessionID(context.TODO())) c.JsonResult(6000, i18n.Tr(c.Lang, "message.need_relogin")) return } book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error(err) c.JsonResult(500, i18n.Tr(c.Lang, "message.item_not_exist")) } if book.BookPassword != password { c.JsonResult(5001, i18n.Tr(c.Lang, "message.wrong_password")) } else { c.SetSession(identify, password) c.JsonResult(0, "OK") } } // 阅读文档 func (c *DocumentController) Read() { identify := c.Ctx.Input.Param(":key") token := c.GetString("token") id := c.GetString(":id") if identify == "" || id == "" { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } // 如果没有开启匿名访问则跳转到登录 if !c.EnableAnonymous && !c.isUserLoggedIn() { promptUserToLogIn(c) return } bookResult := c.isReadable(identify, token) c.TplName = fmt.Sprintf("document/%s_read.tpl", bookResult.Theme) doc := models.NewDocument() if docId, err := strconv.Atoi(id); err == nil { doc, err = doc.FromCacheById(docId) if err != nil || doc == nil { logs.Error("从缓存中读取文档时失败 ->", err) c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.doc_not_exist")) return } } else { doc, err = doc.FromCacheByIdentify(id, bookResult.BookId) if err != nil || doc == nil { if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.doc_not_exist")) } else { logs.Error("从数据库查询文档时出错 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.unknown_exception")) } return } } if doc.BookId != bookResult.BookId { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.doc_not_exist")) } doc.Lang = c.Lang doc.Processor() attach, err := models.NewAttachment().FindListByDocumentId(doc.DocumentId) if err == nil { doc.AttachList = attach } // prev,next treeJson, err := models.NewDocument().FindDocumentTree2(bookResult.BookId) if err != nil { logs.Error("生成项目文档树时出错 ->", err) } res := getTreeRecursive(treeJson, 0) flat := make([]DocumentTreeFlatten, 0) Flatten(res, &flat) var index int for i, v := range flat { if v.Identify == id { index = i } } var PrevName, PrevPath, NextName, NextPath string if index == 0 { c.Data["PrevName"] = "" PrevName = "" } else { c.Data["PrevPath"] = identify + "/" + flat[index-1].Identify c.Data["PrevName"] = flat[index-1].DocumentName PrevPath = identify + "/" + flat[index-1].Identify PrevName = flat[index-1].DocumentName } if index == len(flat)-1 { c.Data["NextName"] = "" NextName = "" } else { c.Data["NextPath"] = identify + "/" + flat[index+1].Identify c.Data["NextName"] = flat[index+1].DocumentName NextPath = identify + "/" + flat[index+1].Identify NextName = flat[index+1].DocumentName } doc.IncrViewCount(doc.DocumentId) doc.ViewCount = doc.ViewCount + 1 doc.PutToCache() if c.IsAjax() { var data struct { DocId int `json:"doc_id"` DocIdentify string `json:"doc_identify"` DocTitle string `json:"doc_title"` Body string `json:"body"` Title string `json:"title"` Version int64 `json:"version"` ViewCount int `json:"view_count"` MarkdownTheme string `json:"markdown_theme"` IsMarkdown bool `json:"is_markdown"` } data.DocId = doc.DocumentId data.DocIdentify = doc.Identify data.DocTitle = doc.DocumentName data.Body = doc.Release + "
"+ i18n.Tr(c.Lang, "doc.prev") + ":
" + i18n.Tr(c.Lang, "doc.next") + ":
" data.Title = doc.DocumentName + " - Powered by MinDoc" data.Version = doc.Version data.ViewCount = doc.ViewCount data.MarkdownTheme = doc.MarkdownTheme if bookResult.Editor == EditorCherryMarkdown { data.IsMarkdown = true } c.JsonResult(0, "ok", data) } else { c.Data["DocumentId"] = doc.DocumentId c.Data["DocIdentify"] = doc.Identify if bookResult.IsDisplayComment { // 获取评论、分页 comments, count, _ := models.NewComment().QueryCommentByDocumentId(doc.DocumentId, 1, conf.PageSize, c.Member) page := pagination.PageUtil(int(count), 1, conf.PageSize, comments) c.Data["Page"] = page } } tree, err := models.NewDocument().CreateDocumentTreeForHtml(bookResult.BookId, doc.DocumentId) if err != nil && err != orm.ErrNoRows { logs.Error("生成项目文档树时出错 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.build_doc_tree_error")) } c.Data["Description"] = utils.AutoSummary(doc.Release, 120) c.Data["Model"] = bookResult c.Data["Result"] = template.HTML(tree) c.Data["Title"] = doc.DocumentName c.Data["Content"] = template.HTML(doc.Release + "
"+ i18n.Tr(c.Lang, "doc.prev") + ":
" + i18n.Tr(c.Lang, "doc.next") + ":
") c.Data["ViewCount"] = doc.ViewCount c.Data["FoldSetting"] = "closed" if bookResult.Editor == EditorCherryMarkdown { c.Data["MarkdownTheme"] = doc.MarkdownTheme } if doc.IsOpen == 1 { c.Data["FoldSetting"] = "open" } else if doc.IsOpen == 2 { c.Data["FoldSetting"] = "empty" } } // 递归得到树状结构体 func getTreeRecursive(list []*models.DocumentTree, parentId int) (res []*models.DocumentTree) { for _, v := range list { if v.ParentId == parentId { v.Children = getTreeRecursive(list, v.DocumentId) res = append(res, v) } } return res } // 递归将树状结构体转换为扁平结构体数组 // func Flatten(list []*models.DocumentTree, flattened *[]DocumentTreeFlatten) (flatten *[]DocumentTreeFlatten) { func Flatten(list []*models.DocumentTree, flattened *[]DocumentTreeFlatten) { // Treeslice := make([]*DocumentTreeFlatten, 0) for _, v := range list { tree := make([]DocumentTreeFlatten, 1) tree[0].DocumentId = v.DocumentId tree[0].DocumentName = v.DocumentName tree[0].Identify = v.Identify *flattened = append(*flattened, tree...) if len(v.Children) > 0 { Flatten(v.Children, flattened) } } return } // 编辑文档 func (c *DocumentController) Edit() { c.Prepare() if c.Member.Role == conf.MemberReaderRole { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } identify := c.Ctx.Input.Param(":key") if identify == "" { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.project_id_error")) } bookResult := models.NewBookResult() var err error // 如果是管理者,则不判断权限 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookResult = models.NewBookResult().ToBookResult(*book) } else { bookResult, err = models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { if err == orm.ErrNoRows || err == models.ErrPermissionDenied { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } else { logs.Error("查询项目时出错 -> ", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) } return } if bookResult.RoleId == conf.BookObserver { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } } c.TplName = fmt.Sprintf("document/%s_edit_template.tpl", bookResult.Editor) c.Data["Model"] = bookResult r, _ := json.Marshal(bookResult) c.Data["ModelResult"] = template.JS(string(r)) c.Data["Result"] = template.JS("[]") trees, err := models.NewDocument().FindDocumentTree(bookResult.BookId) if err != nil { logs.Error("FindDocumentTree => ", err) } else { if len(trees) > 0 { if jtree, err := json.Marshal(trees); err == nil { c.Data["Result"] = template.JS(string(jtree)) } } else { c.Data["Result"] = template.JS("[]") } } c.Data["BaiDuMapKey"] = web.AppConfig.DefaultString("baidumapkey", "") if conf.GetUploadFileSize() > 0 { c.Data["UploadFileSize"] = conf.GetUploadFileSize() } else { c.Data["UploadFileSize"] = "undefined" } } // 创建一个文档 func (c *DocumentController) Create() { identify := c.GetString("identify") docIdentify := c.GetString("doc_identify") docName := c.GetString("doc_name") parentId, _ := c.GetInt("parent_id", 0) docId, _ := c.GetInt("doc_id", 0) isOpen, _ := c.GetInt("is_open", 0) if identify == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if docName == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.doc_name_empty")) } bookId := 0 // 如果是超级管理员则不判断权限 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_existed_or_no_permit")) } bookId = book.BookId } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_existed_or_no_permit")) } bookId = bookResult.BookId } if docIdentify != "" { if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, docIdentify); !ok || err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.project_id_tips")) } d, _ := models.NewDocument().FindByIdentityFirst(docIdentify, bookId) if d.DocumentId > 0 && d.DocumentId != docId { c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_id_existed")) } } if parentId > 0 { doc, err := models.NewDocument().Find(parentId) if err != nil || doc.BookId != bookId { c.JsonResult(6003, i18n.Tr(c.Lang, "message.parent_id_not_existed")) } } document, _ := models.NewDocument().Find(docId) document.MemberId = c.Member.MemberId document.BookId = bookId document.Identify = docIdentify document.Version = time.Now().Unix() document.DocumentName = docName document.ParentId = parentId if isOpen == 1 { document.IsOpen = 1 } else if isOpen == 2 { document.IsOpen = 2 } else { document.IsOpen = 0 } if err := document.InsertOrUpdate(); err != nil { logs.Error("添加或更新文档时出错 -> ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } else { c.JsonResult(0, "ok", document) } } // 上传附件或图片 func (c *DocumentController) Upload() { identify := c.GetString("identify") docId, _ := c.GetInt("doc_id") isAttach := true if identify == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } names := []string{"editormd-file-file", "editormd-image-file", "file", "editormd-resource-file"} var files []*multipart.FileHeader for _, name := range names { file, err := c.GetFiles(name) if err != nil { continue } if len(file) > 0 && err == nil { files = append(files, file...) } } if len(files) == 0 { c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_empty")) return } result2 := []map[string]interface{}{} var result map[string]interface{} for i, _ := range files { //for each fileheader, get a handle to the actual file file, err := files[i].Open() defer file.Close() if err != nil { c.JsonResult(6002, err.Error()) } // defer file.Close() type Size interface { Size() int64 } // if conf.GetUploadFileSize() > 0 && moreFile.Size > conf.GetUploadFileSize() { if conf.GetUploadFileSize() > 0 && files[i].Size > conf.GetUploadFileSize() { c.JsonResult(6009, i18n.Tr(c.Lang, "message.upload_file_size_limit")) } // ext := filepath.Ext(moreFile.Filename) ext := filepath.Ext(files[i].Filename) //文件必须带有后缀名 if ext == "" { c.JsonResult(6003, i18n.Tr(c.Lang, "message.upload_file_type_error")) } //如果文件类型设置为 * 标识不限制文件类型 if conf.IsAllowUploadFileExt(ext) == false { c.JsonResult(6004, i18n.Tr(c.Lang, "message.upload_file_type_error")) } bookId := 0 // 如果是超级管理员,则不判断权限 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.doc_not_exist_or_no_permit")) } bookId = book.BookId } else { book, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil { logs.Error("DocumentController.Edit => ", err) if err == orm.ErrNoRows { c.JsonResult(6006, i18n.Tr(c.Lang, "message.no_permission")) } c.JsonResult(6001, err.Error()) } // 如果没有编辑权限 if book.RoleId != conf.BookEditor && book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder { c.JsonResult(6006, i18n.Tr(c.Lang, "message.no_permission")) } bookId = book.BookId } if docId > 0 { doc, err := models.NewDocument().Find(docId) if err != nil { c.JsonResult(6007, i18n.Tr(c.Lang, "message.doc_not_exist")) } if doc.BookId != bookId { c.JsonResult(6008, i18n.Tr(c.Lang, "message.doc_not_belong_project")) } } fileName := "m_" + cryptil.UniqueId() + "_r" filePath := filepath.Join(conf.WorkingDirectory, "uploads", identify) //将图片和文件分开存放 attachment := models.NewAttachment() var strategy filetil.FileTypeStrategy if filetil.IsImageExt(files[i].Filename) { strategy = filetil.ImageStrategy{} attachment.ResourceType = "image" } else if filetil.IsVideoExt(files[i].Filename) { strategy = filetil.VideoStrategy{} attachment.ResourceType = "video" } else { strategy = filetil.DefaultStrategy{} attachment.ResourceType = "file" } filePath = strategy.GetFilePath(filePath, fileName, ext) path := filepath.Dir(filePath) _ = os.MkdirAll(path, os.ModePerm) //copy the uploaded file to the destination file dst, err := os.Create(filePath) defer dst.Close() if _, err := io.Copy(dst, file); err != nil { logs.Error("保存文件失败 -> ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } attachment.BookId = bookId // attachment.FileName = moreFile.Filename attachment.FileName = files[i].Filename attachment.CreateAt = c.Member.MemberId attachment.FileExt = ext attachment.FilePath = strings.TrimPrefix(filePath, conf.WorkingDirectory) attachment.DocumentId = docId if fileInfo, err := os.Stat(filePath); err == nil { attachment.FileSize = float64(fileInfo.Size()) } if docId > 0 { attachment.DocumentId = docId } if filetil.IsImageExt(files[i].Filename) || filetil.IsVideoExt(files[i].Filename) { attachment.HttpPath = "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1) if strings.HasPrefix(attachment.HttpPath, "//") { attachment.HttpPath = conf.URLForWithCdnImage(string(attachment.HttpPath[1:])) } isAttach = false } err = attachment.Insert() if err != nil { os.Remove(filePath) logs.Error("文件保存失败 ->", err) c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed")) } if attachment.HttpPath == "" { attachment.HttpPath = conf.URLForNotHost("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId) if err := attachment.Update(); err != nil { logs.Error("保存文件失败 ->", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } } result = map[string]interface{}{ "errcode": 0, "success": 1, "message": "ok", "url": attachment.HttpPath, "link": attachment.HttpPath, "alt": attachment.FileName, "is_attach": isAttach, "attach": attachment, "resource_type": attachment.ResourceType, } result2 = append(result2, result) } if len(files) == 1 { // froala单文件上传 c.Ctx.Output.JSON(result, true, false) } else { c.Ctx.Output.JSON(result2, true, false) } c.StopRun() } // 下载附件 func (c *DocumentController) DownloadAttachment() { c.Prepare() identify := c.Ctx.Input.Param(":key") attachId, _ := strconv.Atoi(c.Ctx.Input.Param(":attach_id")) token := c.GetString("token") memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } bookId := 0 // 判断用户是否参与了项目 bookResult, err := models.NewBookResult().FindByIdentify(identify, memberId) if err != nil { // 判断项目公开状态 book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } else { logs.Error("查找项目时出错 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) } } // 如果不是超级管理员则判断权限 if c.Member == nil || c.Member.Role != conf.MemberSuperRole { // 如果项目是私有的,并且 token 不正确 if (book.PrivatelyOwned == 1 && token == "") || (book.PrivatelyOwned == 1 && book.PrivateToken != token) { c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) } } bookId = book.BookId } else { bookId = bookResult.BookId } // 查找附件 attachment, err := models.NewAttachment().Find(attachId) if err != nil { logs.Error("查找附件时出错 -> ", err) if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } else { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) } } if attachment.BookId != bookId { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.attachment_not_exist")) } c.Ctx.Output.Download(filepath.Join(conf.WorkingDirectory, attachment.FilePath), attachment.FileName) c.StopRun() } // 删除附件 func (c *DocumentController) RemoveAttachment() { c.Prepare() attachId, _ := c.GetInt("attach_id") if attachId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } attach, err := models.NewAttachment().Find(attachId) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.attachment_not_exist")) } document, err := models.NewDocument().Find(attach.DocumentId) if err != nil { logs.Error(err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.doc_not_exist")) } if c.Member.Role != conf.MemberSuperRole { rel, err := models.NewRelationship().FindByBookIdAndMemberId(document.BookId, c.Member.MemberId) if err != nil { logs.Error(err) c.JsonResult(6004, i18n.Tr(c.Lang, "message.no_permission")) } if rel.RoleId == conf.BookObserver { c.JsonResult(6004, i18n.Tr(c.Lang, "message.no_permission")) } } err = attach.Delete() if err != nil { logs.Error(err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } os.Remove(filepath.Join(conf.WorkingDirectory, attach.FilePath)) c.JsonResult(0, "ok", attach) } // 删除文档 func (c *DocumentController) Delete() { c.Prepare() identify := c.GetString("identify") docId, err := c.GetInt("doc_id", 0) bookId := 0 // 如果是超级管理员则忽略权限判断 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = book.BookId } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = bookResult.BookId } if docId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } doc, err := models.NewDocument().Find(docId) if err != nil { logs.Error("Delete => ", err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } // 如果文档所属项目错误 if doc.BookId != bookId { c.JsonResult(6004, i18n.Tr(c.Lang, "message.param_error")) } // 递归删除项目下的文档以及子文档 err = doc.RecursiveDocument(doc.DocumentId) if err != nil { c.JsonResult(6005, i18n.Tr(c.Lang, "message.failed")) } // 重置文档数量统计 models.NewBook().ResetDocumentNumber(doc.BookId) c.JsonResult(0, "ok") } // 获取文档内容 func (c *DocumentController) Content() { c.Prepare() identify := c.Ctx.Input.Param(":key") docId, err := c.GetInt("doc_id") if err != nil { docId, _ = strconv.Atoi(c.Ctx.Input.Param(":id")) } bookId := 0 autoRelease := false // 如果是超级管理员,则忽略权限 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil || book == nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) return } bookId = book.BookId autoRelease = book.AutoRelease == 1 } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("项目不存在或权限不足 -> ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = bookResult.BookId autoRelease = bookResult.AutoRelease } if docId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if c.Ctx.Input.IsPost() { markdown := strings.TrimSpace(c.GetString("markdown", "")) content := c.GetString("html") markdownTheme := c.GetString("markdown_theme", "theme__light") version, _ := c.GetInt64("version", 0) isCover := c.GetString("cover") doc, err := models.NewDocument().Find(docId) if err != nil || doc == nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.read_file_error")) return } if doc.BookId != bookId { c.JsonResult(6004, i18n.Tr(c.Lang, "message.dock_not_belong_project")) } if doc.Version != version && !strings.EqualFold(isCover, "yes") { logs.Info("%d|", version, doc.Version) c.JsonResult(6005, i18n.Tr(c.Lang, "message.confirm_override_doc")) } history := models.NewDocumentHistory() history.DocumentId = docId history.Content = doc.Content history.Markdown = doc.Markdown history.DocumentName = doc.DocumentName history.ModifyAt = c.Member.MemberId history.MemberId = doc.MemberId history.ParentId = doc.ParentId history.Version = time.Now().Unix() history.Action = "modify" history.ActionName = i18n.Tr(c.Lang, "doc.modify_doc") if markdown == "" && content != "" { doc.Markdown = content } else { doc.Markdown = markdown doc.MarkdownTheme = markdownTheme } doc.Version = time.Now().Unix() doc.Content = content doc.ModifyAt = c.Member.MemberId if err := doc.InsertOrUpdate(); err != nil { logs.Error("InsertOrUpdate => ", err) c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed")) } // 如果启用了文档历史,则添加历史文档 ///如果两次保存的MD5值不同则保存为历史,否则忽略 go func(history *models.DocumentHistory) { if c.EnableDocumentHistory && cryptil.Md5Crypt(history.Markdown) != cryptil.Md5Crypt(doc.Markdown) { _, err = history.InsertOrUpdate() if err != nil { logs.Error("DocumentHistory InsertOrUpdate => ", err) } } }(history) //如果启用了自动发布 if autoRelease { go func() { doc.Lang = c.Lang err := doc.ReleaseContent() if err == nil { logs.Informational(i18n.Tr(c.Lang, "message.doc_auto_published")+"-> document_id=%d;document_name=%s", doc.DocumentId, doc.DocumentName) } }() } c.JsonResult(0, "ok", doc) } doc, err := models.NewDocument().Find(docId) if err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.doc_not_exist")) return } attach, err := models.NewAttachment().FindListByDocumentId(doc.DocumentId) if err == nil { doc.AttachList = attach } c.JsonResult(0, "ok", doc) } // Export 导出 func (c *DocumentController) Export() { c.Prepare() identify := c.Ctx.Input.Param(":key") if identify == "" { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.param_error")) } output := c.GetString("output") token := c.GetString("token") // 如果没有开启匿名访问则跳转到登录 if !c.EnableAnonymous && !c.isUserLoggedIn() { promptUserToLogIn(c) return } if !conf.GetEnableExport() { c.ShowErrorPage(500, i18n.Tr(c.Lang, "export_func_disable")) } bookResult := models.NewBookResult() if c.Member != nil && c.Member.IsAdministrator() { book, err := models.NewBook().FindByIdentify(identify) if err != nil { if err == orm.ErrNoRows { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } else { logs.Error("查找项目时出错 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) } } bookResult = models.NewBookResult().ToBookResult(*book) } else { bookResult = c.isReadable(identify, token) } if !bookResult.IsDownload { c.ShowErrorPage(200, i18n.Tr(c.Lang, "message.cur_project_export_func_disable")) } if !strings.HasPrefix(bookResult.Cover, "http:://") && !strings.HasPrefix(bookResult.Cover, "https:://") { bookResult.Cover = conf.URLForWithCdnImage(bookResult.Cover) } if output == Markdown { if bookResult.Editor != EditorMarkdown && bookResult.Editor != EditorCherryMarkdown { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.cur_project_not_support_md")) } p, err := bookResult.ExportMarkdown(c.CruSession.SessionID(context.TODO())) if err != nil { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.failed")) } c.Ctx.Output.Download(p, bookResult.BookName+".zip") c.StopRun() return } outputPath := filepath.Join(conf.GetExportOutputPath(), strconv.Itoa(bookResult.BookId)) pdfpath := filepath.Join(outputPath, "book.pdf") epubpath := filepath.Join(outputPath, "book.epub") mobipath := filepath.Join(outputPath, "book.mobi") docxpath := filepath.Join(outputPath, "book.docx") if output == "pdf" && filetil.FileExists(pdfpath) { c.Ctx.Output.Download(pdfpath, bookResult.BookName+".pdf") c.Abort("200") } else if output == "epub" && filetil.FileExists(epubpath) { c.Ctx.Output.Download(epubpath, bookResult.BookName+".epub") c.Abort("200") } else if output == "mobi" && filetil.FileExists(mobipath) { c.Ctx.Output.Download(mobipath, bookResult.BookName+".mobi") c.Abort("200") } else if output == "docx" && filetil.FileExists(docxpath) { c.Ctx.Output.Download(docxpath, bookResult.BookName+".docx") c.Abort("200") } else if output == "pdf" || output == "epub" || output == "docx" || output == "mobi" { if err := models.BackgroundConvert(c.CruSession.SessionID(context.TODO()), bookResult); err != nil && err != gopool.ErrHandlerIsExist { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.export_failed")) } c.ShowErrorPage(200, i18n.Tr(c.Lang, "message.file_converting")) } else { c.ShowErrorPage(200, i18n.Tr(c.Lang, "message.unsupport_file_type")) } c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.no_exportable_file")) } // 生成项目访问的二维码 func (c *DocumentController) QrCode() { c.Prepare() identify := c.GetString(":key") book, err := models.NewBook().FindByIdentify(identify) if err != nil || book.BookId <= 0 { c.ShowErrorPage(404, i18n.Tr(c.Lang, "message.item_not_exist")) } uri := conf.URLFor("DocumentController.Index", ":key", identify) code, err := qr.Encode(uri, qr.L, qr.Unicode) if err != nil { logs.Error("生成二维码失败 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.gen_qrcode_failed")) } code, err = barcode.Scale(code, 150, 150) if err != nil { logs.Error("生成二维码失败 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.gen_qrcode_failed")) } c.Ctx.ResponseWriter.Header().Set("Content-Type", "image/png") // imgpath := filepath.Join("cache","qrcode",identify + ".png") err = png.Encode(c.Ctx.ResponseWriter, code) if err != nil { logs.Error("生成二维码失败 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.gen_qrcode_failed")) } } // 项目内搜索 func (c *DocumentController) Search() { c.Prepare() identify := c.Ctx.Input.Param(":key") token := c.GetString("token") keyword := strings.TrimSpace(c.GetString("keyword")) if identify == "" { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if !c.EnableAnonymous && !c.isUserLoggedIn() { promptUserToLogIn(c) return } bookResult := c.isReadable(identify, token) docs, err := models.NewDocumentSearchResult().SearchDocument(keyword, bookResult.BookId) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.search_result_error")) } if len(docs) < 0 { c.JsonResult(404, i18n.Tr(c.Lang, "message.no_data")) } for _, doc := range docs { doc.BookId = bookResult.BookId doc.BookName = bookResult.BookName doc.Description = bookResult.Description doc.BookIdentify = bookResult.Identify } c.JsonResult(0, "ok", docs) } // 文档历史列表 func (c *DocumentController) History() { c.Prepare() c.TplName = "document/history.tpl" identify := c.GetString("identify") docId, err := c.GetInt("doc_id", 0) pageIndex, _ := c.GetInt("page", 1) bookId := 0 // 如果是超级管理员则忽略权限判断 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("查找项目失败 ->", err) c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit") return } bookId = book.BookId c.Data["Model"] = book } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("查找项目失败 ->", err) c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit") return } bookId = bookResult.BookId c.Data["Model"] = bookResult } if docId <= 0 { c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.param_error") return } doc, err := models.NewDocument().Find(docId) if err != nil { logs.Error("Delete => ", err) c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.get_doc_his_failed") return } // 如果文档所属项目错误 if doc.BookId != bookId { c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.param_error") return } histories, totalCount, err := models.NewDocumentHistory().FindToPager(docId, pageIndex, conf.PageSize) if err != nil { logs.Error("分页查找文档历史失败 ->", err) c.Data["ErrorMessage"] = i18n.Tr(c.Lang, "message.get_doc_his_failed") return } c.Data["List"] = histories c.Data["PageHtml"] = "" c.Data["Document"] = doc if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } } func (c *DocumentController) DeleteHistory() { c.Prepare() c.TplName = "document/history.tpl" identify := c.GetString("identify") docId, err := c.GetInt("doc_id", 0) historyId, _ := c.GetInt("history_id", 0) if historyId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } bookId := 0 // 如果是超级管理员则忽略权限判断 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("查找项目失败 ->", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = book.BookId } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("查找项目失败 ->", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = bookResult.BookId } if docId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } doc, err := models.NewDocument().Find(docId) if err != nil { logs.Error("Delete => ", err) c.JsonResult(6001, i18n.Tr(c.Lang, "message.get_doc_his_failed")) } // 如果文档所属项目错误 if doc.BookId != bookId { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } err = models.NewDocumentHistory().Delete(historyId, docId) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } // 通过文档历史恢复文档 func (c *DocumentController) RestoreHistory() { c.Prepare() c.TplName = "document/history.tpl" identify := c.GetString("identify") docId, err := c.GetInt("doc_id", 0) historyId, _ := c.GetInt("history_id", 0) if historyId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } bookId := 0 // 如果是超级管理员则忽略权限判断 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = book.BookId } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("FindByIdentify => ", err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist_or_no_permit")) } bookId = bookResult.BookId } if docId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } doc, err := models.NewDocument().Find(docId) if err != nil { logs.Error("Delete => ", err) c.JsonResult(6001, i18n.Tr(c.Lang, "message.get_doc_his_failed")) } // 如果文档所属项目错误 if doc.BookId != bookId { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } err = models.NewDocumentHistory().Restore(historyId, docId, c.Member.MemberId) if err != nil { logs.Error(err) c.JsonResult(6002, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok", doc) } func (c *DocumentController) Compare() { c.Prepare() c.TplName = "document/compare.tpl" historyId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) identify := c.Ctx.Input.Param(":key") bookId := 0 editor := EditorMarkdown // 如果是超级管理员则忽略权限判断 if c.Member.IsAdministrator() { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error("DocumentController.Compare => ", err) c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) return } bookId = book.BookId c.Data["Model"] = book editor = book.Editor } else { bookResult, err := models.NewBookResult().FindByIdentify(identify, c.Member.MemberId) if err != nil || bookResult.RoleId == conf.BookObserver { logs.Error("FindByIdentify => ", err) c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) return } bookId = bookResult.BookId c.Data["Model"] = bookResult editor = bookResult.Editor } if historyId <= 0 { c.ShowErrorPage(60002, i18n.Tr(c.Lang, "message.param_error")) } history, err := models.NewDocumentHistory().Find(historyId) if err != nil { logs.Error("DocumentController.Compare => ", err) c.ShowErrorPage(60003, err.Error()) } doc, err := models.NewDocument().Find(history.DocumentId) if err != nil || doc == nil || doc.BookId != bookId { c.ShowErrorPage(60002, i18n.Tr(c.Lang, "message.doc_not_exist")) return } c.Data["HistoryId"] = historyId c.Data["DocumentId"] = doc.DocumentId if editor == EditorMarkdown || editor == EditorCherryMarkdown { c.Data["HistoryContent"] = history.Markdown c.Data["Content"] = doc.Markdown } else { c.Data["HistoryContent"] = template.HTML(history.Content) c.Data["Content"] = template.HTML(doc.Content) } } // 判断用户是否可以阅读文档 func (c *DocumentController) isReadable(identify, token string) *models.BookResult { book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { logs.Error(err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.item_not_exist")) } bookResult := models.NewBookResult().ToBookResult(*book) isOk := false if c.isUserLoggedIn() { roleId, err := models.NewBook().FindForRoleId(book.BookId, c.Member.MemberId) if err == nil { isOk = true bookResult.MemberId = c.Member.MemberId bookResult.RoleId = roleId } } /* 私有项目: * 管理员可以直接访问 * 参与者可以直接访问 * 其他用户(支持匿名访问) * token设置情况 * 已设置:可以通过token访问 * 未设置:不可以通过token访问 * password设置情况 * 已设置:可以通过password访问 * 未设置:不可以通过password访问 * 注意: * 1. 第一次访问需要存session * 2. 有session优先使用session中的token或者password,再使用携带的token或者password * 3. 私有项目如果token和password都没有设置,则除管理员和参与者的其他用户不可以访问 * 4. 使用token访问如果不通过,则提示输入密码 */ if book.PrivatelyOwned == 1 { if c.isUserLoggedIn() && c.Member.IsAdministrator() { return bookResult } if isOk { // Project participant. return bookResult } // Use session in preference. if tokenOrPassword, ok := c.GetSession(identify).(string); ok { if strings.EqualFold(book.PrivateToken, tokenOrPassword) || strings.EqualFold(book.BookPassword, tokenOrPassword) { return bookResult } } // Next: Session not exist or not correct. if book.PrivateToken != "" && book.PrivateToken == token { c.SetSession(identify, token) return bookResult } else if book.BookPassword != "" { // Send a page for inputting password. // For verification, see function DocumentController.CheckPassword body, err := c.ExecuteViewPathTemplate("document/document_password.tpl", map[string]string{"Identify": book.Identify, "Lang": c.Lang}) if err != nil { logs.Error("显示密码页面失败 ->", err) c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.system_error")) } c.CustomAbort(200, body) } else { // No permission to access this book. logs.Info("尝试访问文档但权限不足 ->", identify, token) c.ShowErrorPage(403, i18n.Tr(c.Lang, "message.no_permission")) } } return bookResult } func promptUserToLogIn(c *DocumentController) { logs.Info("Access " + c.Ctx.Request.URL.RequestURI() + " not permitted.") logs.Info(" Access will be redirected to login page(SessionId: " + c.CruSession.SessionID(context.TODO()) + ").") if c.IsAjax() { c.JsonResult(6000, i18n.Tr(c.Lang, "message.need_relogin")) } else { c.Redirect(conf.URLFor("AccountController.Login")+"?url="+url.PathEscape(conf.BaseUrl+c.Ctx.Request.URL.RequestURI()), 302) } } ================================================ FILE: controllers/ErrorController.go ================================================ package controllers type ErrorController struct { BaseController } func (c *ErrorController) Error404() { c.TplName = "errors/404.tpl" } func (c *ErrorController) Error403() { c.TplName = "errors/403.tpl" } func (c *ErrorController) Error500() { c.TplName = "errors/error.tpl" } ================================================ FILE: controllers/HomeController.go ================================================ package controllers import ( "math" "net/url" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils/pagination" ) type HomeController struct { BaseController } func (c *HomeController) Prepare() { c.BaseController.Prepare() //如果没有开启匿名访问,则跳转到登录页面 if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login")+"?url="+url.PathEscape(conf.BaseUrl+c.Ctx.Request.URL.RequestURI()), 302) } } func (c *HomeController) Index() { c.Prepare() c.TplName = "home/index.tpl" pageIndex, _ := c.GetInt("page", 1) pageSize := 18 memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } books, totalCount, err := models.NewBook().FindForHomeToPager(pageIndex, pageSize, memberId) if err != nil { logs.Error(err) c.Abort("500") } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, pageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["TotalPages"] = int(math.Ceil(float64(totalCount) / float64(pageSize))) c.Data["Lists"] = books } ================================================ FILE: controllers/ItemsetsController.go ================================================ package controllers import ( "math" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils/pagination" ) type ItemsetsController struct { BaseController } func (c *ItemsetsController) Prepare() { c.BaseController.Prepare() //如果没有开启你们访问则跳转到登录 if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login"), 302) return } } func (c *ItemsetsController) Index() { c.Prepare() c.TplName = "items/index.tpl" pageSize := 16 pageIndex, _ := c.GetInt("page", 0) items, totalCount, err := models.NewItemsets().FindToPager(pageIndex, pageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if err == orm.ErrNoRows || len(items) <= 0 { c.Data["Lists"] = items c.Data["PageHtml"] = "" return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, pageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["TotalPages"] = int(math.Ceil(float64(totalCount) / float64(pageSize))) c.Data["Lists"] = items } func (c *ItemsetsController) List() { c.Prepare() c.TplName = "items/list.tpl" pageSize := 18 itemKey := c.Ctx.Input.Param(":key") pageIndex, _ := c.GetInt("page", 1) if itemKey == "" { c.Abort("404") } item, err := models.NewItemsets().FindFirst(itemKey) if err != nil { if err == orm.ErrNoRows { c.Abort("404") } else { logs.Error(err) c.Abort("500") } } memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } searchResult, totalCount, err := models.NewItemsets().FindItemsetsByItemKey(itemKey, pageIndex, pageSize, memberId) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, "查询文档列表时出错") } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, pageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["TotalPages"] = int(math.Ceil(float64(totalCount) / float64(pageSize))) c.Data["Lists"] = searchResult c.Data["Model"] = item } ================================================ FILE: controllers/LabelController.go ================================================ package controllers import ( "math" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils/pagination" ) type LabelController struct { BaseController } func (c *LabelController) Prepare() { c.BaseController.Prepare() //如果没有开启你们访问则跳转到登录 if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login"), 302) return } } //查看包含标签的文档列表. func (c *LabelController) Index() { c.Prepare() c.TplName = "label/index.tpl" labelName := c.Ctx.Input.Param(":key") pageIndex, _ := c.GetInt("page", 1) if labelName == "" { c.Abort("404") } _, err := models.NewLabel().FindFirst("label_name", labelName) if err != nil { if err == orm.ErrNoRows { c.Abort("404") } else { logs.Error(err) c.Abort("500") } } memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } searchResult, totalCount, err := models.NewBook().FindForLabelToPager(labelName, pageIndex, conf.PageSize, memberId) if err != nil && err != orm.ErrNoRows { logs.Error("查询标签时出错 ->", err) c.ShowErrorPage(500, "查询文档列表时出错") } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["Lists"] = searchResult c.Data["LabelName"] = labelName } func (c *LabelController) List() { c.Prepare() c.TplName = "label/list.tpl" pageIndex, _ := c.GetInt("page", 1) pageSize := 200 labels, totalCount, err := models.NewLabel().FindToPager(pageIndex, pageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["TotalPages"] = int(math.Ceil(float64(totalCount) / float64(pageSize))) c.Data["Labels"] = labels } ================================================ FILE: controllers/ManagerController.go ================================================ package controllers import ( "encoding/json" "html/template" "regexp" "strings" "math" "path/filepath" "strconv" "io/ioutil" "os" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/filetil" "github.com/mindoc-org/mindoc/utils/pagination" "github.com/russross/blackfriday/v2" ) type ManagerController struct { BaseController } func (c *ManagerController) Prepare() { c.BaseController.Prepare() if !c.Member.IsAdministrator() { c.Abort("403") } } func (c *ManagerController) Index() { c.TplName = "manager/index.tpl" c.Data["Model"] = models.NewDashboard().Query() c.Data["Action"] = "index" } // 用户列表. func (c *ManagerController) Users() { c.TplName = "manager/users.tpl" c.Data["Action"] = "users" pageIndex, _ := c.GetInt("page", 0) tempMember := models.NewMember() tempMember.Lang = c.Lang members, totalCount, err := tempMember.FindToPager(pageIndex, conf.PageSize) if err != nil { c.Data["ErrorMessage"] = err.Error() return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() for _, item := range members { item.Avatar = conf.URLForWithCdnImage(item.Avatar) } } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(members) if err != nil { c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } // 添加用户. func (c *ManagerController) CreateMember() { account := strings.TrimSpace(c.GetString("account")) password1 := strings.TrimSpace(c.GetString("password1")) password2 := strings.TrimSpace(c.GetString("password2")) email := strings.TrimSpace(c.GetString("email")) phone := strings.TrimSpace(c.GetString("phone")) role, _ := c.GetInt("role", 1) status, _ := c.GetInt("status", 0) if ok, err := regexp.MatchString(conf.RegexpAccount, account); account == "" || !ok || err != nil { c.JsonResult(6001, i18n.Tr(c.Lang, "message.username_invalid_format")) } if l := strings.Count(password1, ""); password1 == "" || l > 50 || l < 6 { c.JsonResult(6002, i18n.Tr(c.Lang, "message.pwd_length_tips")) } if password1 != password2 { c.JsonResult(6003, i18n.Tr(c.Lang, "message.wrong_confirm_pwd")) } if ok, err := regexp.MatchString(conf.RegexpEmail, email); !ok || err != nil || email == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.email_invalid_format")) } if role != 0 && role != 1 && role != 2 { role = 1 } if status != 0 && status != 1 { status = 0 } member := models.NewMember() if _, err := member.FindByAccount(account); err == nil && member.MemberId > 0 { c.JsonResult(6005, i18n.Tr(c.Lang, "message.account_existed")) } member.Account = account member.Password = password1 member.Role = conf.SystemRole(role) member.Avatar = conf.GetDefaultAvatar() member.CreateAt = c.Member.MemberId member.Email = email member.RealName = strings.TrimSpace(c.GetString("real_name", "")) member.Lang = c.Lang if phone != "" { member.Phone = phone } if err := member.Add(); err != nil { c.JsonResult(6006, err.Error()) } c.JsonResult(0, "ok", member) } // 更新用户状态. func (c *ManagerController) UpdateMemberStatus() { c.Prepare() member_id, _ := c.GetInt("member_id", 0) status, _ := c.GetInt("status", 0) if member_id <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if status != 0 && status != 1 { status = 0 } member := models.NewMember() if _, err := member.Find(member_id); err != nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.user_not_existed")) } if member.MemberId == c.Member.MemberId { c.JsonResult(6004, i18n.Tr(c.Lang, "message.cannot_change_own_status")) } if member.Role == conf.MemberSuperRole { c.JsonResult(6005, i18n.Tr(c.Lang, "message.cannot_change_super_status")) } member.Status = status if err := member.Update(); err != nil { logs.Error("", err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok", member) } // 变更用户权限. func (c *ManagerController) ChangeMemberRole() { c.Prepare() memberId, _ := c.GetInt("member_id", 0) role, _ := c.GetInt("role", 0) if memberId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } if role != int(conf.MemberAdminRole) && role != int(conf.MemberGeneralRole) && role != int(conf.MemberReaderRole) { c.JsonResult(6001, i18n.Tr(c.Lang, "message.no_permission")) } member := models.NewMember() if _, err := member.Find(memberId); err != nil { c.JsonResult(6002, i18n.Tr(c.Lang, "message.user_not_existed")) } if member.MemberId == c.Member.MemberId { c.JsonResult(6004, i18n.Tr(c.Lang, "message.cannot_change_own_priv")) } if member.Role == conf.MemberSuperRole { c.JsonResult(6005, i18n.Tr(c.Lang, "message.cannot_change_super_priv")) } member.Role = conf.SystemRole(role) if err := member.Update(); err != nil { c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } member.Lang = c.Lang member.ResolveRoleName() c.JsonResult(0, "ok", member) } // 编辑用户信息. func (c *ManagerController) EditMember() { c.Prepare() c.TplName = "manager/edit_users.tpl" c.Data["Action"] = "users" member_id, _ := c.GetInt(":id", 0) if member_id <= 0 { c.Abort("404") } member, err := models.NewMember().Find(member_id) if err != nil { logs.Error(err) c.Abort("404") } if c.Ctx.Input.IsPost() { password1 := c.GetString("password1") password2 := c.GetString("password2") email := c.GetString("email") phone := c.GetString("phone") description := c.GetString("description") member.Email = email member.Phone = phone member.Description = description member.RealName = c.GetString("real_name") if password1 != "" && password2 != password1 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.wrong_confirm_pwd")) } if password1 != "" && member.AuthMethod != conf.AuthMethodLDAP { member.Password = password1 } if err := member.Valid(password1 == ""); err != nil { c.JsonResult(6002, err.Error()) } if password1 != "" { password, err := utils.PasswordHash(password1) if err != nil { logs.Error(err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.pwd_encrypt_failed")) } member.Password = password } if err := member.Update(); err != nil { c.JsonResult(6004, err.Error()) } c.JsonResult(0, "ok") } c.Data["Model"] = member } // 删除一个用户,并将该用户的所有信息转移到超级管理员上. func (c *ManagerController) DeleteMember() { c.Prepare() member_id, _ := c.GetInt("id", 0) if member_id <= 0 { c.JsonResult(404, i18n.Tr(c.Lang, "message.param_error")) } member, err := models.NewMember().Find(member_id) if err != nil { logs.Error(err) c.JsonResult(500, i18n.Tr(c.Lang, "message.user_not_existed")) } if member.Role == conf.MemberSuperRole { c.JsonResult(500, "不能删除超级管理员") } superMember, err := models.NewMember().FindByFieldFirst("role", 0) if err != nil { logs.Error(err) c.JsonResult(5001, "未能找到超级管理员") } err = models.NewMember().Delete(member_id, superMember.MemberId) if err != nil { logs.Error(err) c.JsonResult(5002, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } // 项目列表. func (c *ManagerController) Books() { c.Prepare() c.TplName = "manager/books.tpl" c.Data["Action"] = "books" pageIndex, _ := c.GetInt("page", 1) books, totalCount, err := models.NewBookResult().FindToPager(pageIndex, conf.PageSize) if err != nil { c.Abort("500") } if totalCount > 0 { //html := utils.GetPagerHtml(c.Ctx.Request.RequestURI, pageIndex, 8, totalCount) pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } for i, book := range books { books[i].Description = utils.StripTags(string(blackfriday.Run([]byte(book.Description)))) books[i].ModifyTime = book.ModifyTime.Local() books[i].CreateTime = book.CreateTime.Local() } c.Data["Lists"] = books } // 编辑项目. func (c *ManagerController) EditBook() { c.Prepare() c.TplName = "manager/edit_book.tpl" c.Data["Action"] = "books" identify := c.GetString(":key") if identify == "" { c.Abort("404") } book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.Abort("500") } if c.Ctx.Input.IsPost() { bookName := strings.TrimSpace(c.GetString("book_name")) description := strings.TrimSpace(c.GetString("description", "")) commentStatus := c.GetString("comment_status") tag := strings.TrimSpace(c.GetString("label")) orderIndex, _ := c.GetInt("order_index", 0) isDownload := strings.TrimSpace(c.GetString("is_download")) == "on" enableShare := strings.TrimSpace(c.GetString("enable_share")) == "on" isUseFirstDocument := strings.TrimSpace(c.GetString("is_use_first_document")) == "on" autoRelease := strings.TrimSpace(c.GetString("auto_release")) == "on" publisher := strings.TrimSpace(c.GetString("publisher")) historyCount, _ := c.GetInt("history_count", 0) itemId, _ := c.GetInt("itemId") if strings.Count(description, "") > 500 { c.JsonResult(6004, i18n.Tr(c.Lang, "message.project_desc_tips")) } if commentStatus != "open" && commentStatus != "closed" && commentStatus != "group_only" && commentStatus != "registered_only" { commentStatus = "closed" } if tag != "" { tags := strings.Split(tag, ";") if len(tags) > 10 { c.JsonResult(6005, "最多允许添加10个标签") } } if !models.NewItemsets().Exist(itemId) { c.JsonResult(6006, i18n.Tr(c.Lang, "message.project_space_not_exist")) } book.Publisher = publisher book.HistoryCount = historyCount book.BookName = bookName book.Description = description book.CommentStatus = commentStatus book.Label = tag book.OrderIndex = orderIndex book.ItemId = itemId book.BookPassword = strings.TrimSpace(c.GetString("bPassword")) if autoRelease { book.AutoRelease = 1 } else { book.AutoRelease = 0 } if isDownload { book.IsDownload = 0 } else { book.IsDownload = 1 } if enableShare { book.IsEnableShare = 0 } else { book.IsEnableShare = 1 } if isUseFirstDocument { book.IsUseFirstDocument = 1 } else { book.IsUseFirstDocument = 0 } if err := book.Update(); err != nil { c.JsonResult(6006, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } if book.PrivateToken != "" { book.PrivateToken = conf.URLFor("DocumentController.Index", ":key", book.Identify, "token", book.PrivateToken) } bookResult := models.NewBookResult() bookResult.ToBookResult(*book) c.Data["Model"] = bookResult } // 删除项目. func (c *ManagerController) DeleteBook() { c.Prepare() bookId, _ := c.GetInt("book_id", 0) if bookId <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } book := models.NewBook() err := book.ThoroughDeleteBook(bookId) if err == orm.ErrNoRows { c.JsonResult(6002, i18n.Tr(c.Lang, "message.item_not_exist")) } if err != nil { logs.Error("删除失败 -> ", err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } // CreateToken 创建访问来令牌. func (c *ManagerController) CreateToken() { c.Prepare() action := c.GetString("action") identify := c.GetString("identify") book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.JsonResult(6001, i18n.Tr(c.Lang, "message.item_not_exist")) } if action == "create" { if book.PrivatelyOwned == 0 { c.JsonResult(6001, "公开项目不能创建阅读令牌") } book.PrivateToken = string(utils.Krand(conf.GetTokenSize(), utils.KC_RAND_KIND_ALL)) if err := book.Update(); err != nil { logs.Error("生成阅读令牌失败 => ", err) c.JsonResult(6003, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok", conf.URLFor("DocumentController.Index", ":key", book.Identify, "token", book.PrivateToken)) } else { book.PrivateToken = "" if err := book.Update(); err != nil { logs.Error("CreateToken => ", err) c.JsonResult(6004, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok", "") } } // 项目设置. func (c *ManagerController) Setting() { c.Prepare() c.TplName = "manager/setting.tpl" c.Data["Action"] = "setting" options, err := models.NewOption().All() if c.Ctx.Input.IsPost() { for _, item := range options { item.OptionValue = c.GetString(item.OptionName) item.InsertOrUpdate() } c.JsonResult(0, "ok") } if err != nil { c.Abort("500") } c.Data["SITE_TITLE"] = c.Option["SITE_NAME"] for _, item := range options { c.Data[item.OptionName] = item.OptionValue } i18nMapStrs, err := web.AppConfig.String("i18n_map") if err != nil { logs.Error("web.AppConfig `i18n_map` not found") i18nMapStrs = "{}" } var i18nMap map[string]string err = json.Unmarshal([]byte(i18nMapStrs), &i18nMap) if err != nil { logs.Error("json `i18nList` Unmarshal fail") i18nMap = make(map[string]string) } c.Data["i18n_map"] = i18nMap } // Transfer 转让项目. func (c *ManagerController) Transfer() { c.Prepare() account := c.GetString("account") if account == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.receive_account_empty")) } member, err := models.NewMember().FindByAccount(account) if err != nil { logs.Error("FindByAccount => ", err) c.JsonResult(6005, i18n.Tr(c.Lang, "message.receive_account_not_exist")) } if member.Status != 0 { c.JsonResult(6006, i18n.Tr(c.Lang, "message.receive_account_disabled")) } if !c.Member.IsAdministrator() { c.Abort("403") } identify := c.GetString("identify") book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.JsonResult(6001, err.Error()) } rel, err := models.NewRelationship().FindFounder(book.BookId) if err != nil { logs.Error("FindFounder => ", err) c.JsonResult(6009, "查询项目创始人失败") } if member.MemberId == rel.MemberId { c.JsonResult(6007, "不能转让给自己") } err = models.NewRelationship().Transfer(book.BookId, rel.MemberId, member.MemberId) if err != nil { logs.Error("Transfer => ", err) c.JsonResult(6008, err.Error()) } c.JsonResult(0, "ok") } func (c *ManagerController) Comments() { c.Prepare() c.TplName = "manager/comments.tpl" if !c.Member.IsAdministrator() { c.Abort("403") } } // DeleteComment 标记评论为已删除 func (c *ManagerController) DeleteComment() { c.Prepare() comment_id, _ := c.GetInt("comment_id", 0) if comment_id <= 0 { c.JsonResult(6001, i18n.Tr(c.Lang, "message.param_error")) } comment := models.NewComment() if _, err := comment.Find(comment_id); err != nil { c.JsonResult(6002, "评论不存在") } comment.Approved = 3 if err := comment.Update("approved"); err != nil { c.JsonResult(6003, "删除评论失败") } c.JsonResult(0, "ok", comment) } // 设置项目私有状态. func (c *ManagerController) PrivatelyOwned() { c.Prepare() status := c.GetString("status") identify := c.GetString("identify") if status != "open" && status != "close" { c.JsonResult(6003, i18n.Tr(c.Lang, "message.param_error")) } state := 0 if status == "open" { state = 0 } else { state = 1 } if !c.Member.IsAdministrator() { c.Abort("403") } book, err := models.NewBook().FindByFieldFirst("identify", identify) if err != nil { c.JsonResult(6001, err.Error()) } book.PrivatelyOwned = state logs.Info("", state, status) err = book.Update() if err != nil { logs.Error("PrivatelyOwned => ", err) c.JsonResult(6004, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } // 附件列表. func (c *ManagerController) AttachList() { c.Prepare() c.TplName = "manager/attach_list.tpl" c.Data["Action"] = "attach" pageIndex, _ := c.GetInt("page", 1) attachList, totalCount, err := models.NewAttachment().FindToPager(pageIndex, conf.PageSize) if err != nil { c.Abort("500") } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } for _, item := range attachList { p := filepath.Join(conf.WorkingDirectory, item.FilePath) item.IsExist = filetil.FileExists(p) } c.Data["Lists"] = attachList } // 附件清理. func (c *ManagerController) AttachClean() { c.Prepare() attachList, _, err := models.NewAttachment().FindToPager(0, 0) if err != nil { c.Abort("500") } for _, item := range attachList { p := filepath.Join(conf.WorkingDirectory, item.FilePath) item.IsExist = filetil.FileExists(p) if item.IsExist { // 判断 searchList, err := models.NewDocumentSearchResult().SearchAllDocument(item.HttpPath) if err != nil { c.Abort("500") } else if len(searchList) == 0 { logs.Info("delete file:", item.FilePath) item.FilePath = p if err := item.Delete(); err != nil { logs.Error("AttachDelete => ", err) c.JsonResult(6002, err.Error()) break } } } } c.JsonResult(0, "ok") } // 附件详情. func (c *ManagerController) AttachDetailed() { c.Prepare() c.TplName = "manager/attach_detailed.tpl" c.Data["Action"] = "attach" attach_id, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) if attach_id <= 0 { c.Abort("404") } attach, err := models.NewAttachmentResult().Find(attach_id) if err != nil { logs.Error("AttachDetailed => ", err) if err == orm.ErrNoRows { c.Abort("404") } else { c.Abort("500") } } attach.FilePath = filepath.Join(conf.WorkingDirectory, attach.FilePath) attach.HttpPath = conf.URLForWithCdnImage(attach.HttpPath) attach.IsExist = filetil.FileExists(attach.FilePath) c.Data["Model"] = attach } // 删除附件. func (c *ManagerController) AttachDelete() { c.Prepare() attachId, _ := c.GetInt("attach_id") if attachId <= 0 { c.Abort("404") } attach, err := models.NewAttachment().Find(attachId) if err != nil { logs.Error("AttachDelete => ", err) c.JsonResult(6001, err.Error()) } attach.FilePath = filepath.Join(conf.WorkingDirectory, attach.FilePath) if err := attach.Delete(); err != nil { logs.Error("AttachDelete => ", err) c.JsonResult(6002, err.Error()) } c.JsonResult(0, "ok") } // 标签列表 func (c *ManagerController) LabelList() { c.Prepare() c.TplName = "manager/label_list.tpl" c.Data["Action"] = "label" pageIndex, _ := c.GetInt("page", 1) labels, totalCount, err := models.NewLabel().FindToPager(pageIndex, conf.PageSize) if err != nil { c.ShowErrorPage(50001, err.Error()) } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["TotalPages"] = int(math.Ceil(float64(totalCount) / float64(conf.PageSize))) c.Data["Lists"] = labels } // 删除标签 func (c *ManagerController) LabelDelete() { labelId, err := strconv.Atoi(c.Ctx.Input.Param(":id")) if err != nil { logs.Error("获取删除标签参数时出错:", err) c.JsonResult(50001, i18n.Tr(c.Lang, "message.param_error")) } if labelId <= 0 { c.JsonResult(50001, i18n.Tr(c.Lang, "message.param_error")) } label, err := models.NewLabel().FindFirst("label_id", labelId) if err != nil { logs.Error("查询标签时出错:", err) c.JsonResult(50001, "查询标签时出错:"+err.Error()) } if err := label.Delete(); err != nil { c.JsonResult(50002, "删除失败:"+err.Error()) } else { c.JsonResult(0, "ok") } } func (c *ManagerController) Config() { c.Prepare() c.TplName = "manager/config.tpl" c.Data["Action"] = "config" if c.Ctx.Input.IsPost() { content := strings.TrimSpace(c.GetString("configFileTextArea")) if content == "" { c.JsonResult(500, "配置文件不能为空") } tf, err := ioutil.TempFile(os.TempDir(), "mindoc") if err != nil { logs.Error("创建临时文件失败 ->", err) c.JsonResult(5001, "创建临时文件失败") } defer tf.Close() tf.WriteString(content) err = web.LoadAppConfig("ini", tf.Name()) if err != nil { logs.Error("加载配置文件失败 ->", err) c.JsonResult(5002, "加载配置文件失败") } err = filetil.CopyFile(tf.Name(), conf.ConfigurationFile) if err != nil { logs.Error("保存配置文件失败 ->", err) c.JsonResult(5003, "保存配置文件失败") } c.JsonResult(0, "保存成功") } c.Data["ConfigContent"] = "" if b, err := ioutil.ReadFile(conf.ConfigurationFile); err == nil { c.Data["ConfigContent"] = string(b) } } func (c *ManagerController) Team() { c.Prepare() c.TplName = "manager/team.tpl" c.Data["Action"] = "team" pageIndex, _ := c.GetInt("page", 0) teams, totalCount, err := models.NewTeam().FindToPager(pageIndex, conf.PageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if err == orm.ErrNoRows || len(teams) <= 0 { c.Data["Result"] = template.JS("[]") c.Data["PageHtml"] = "" return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(teams) if err != nil { c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } func (c *ManagerController) TeamCreate() { c.Prepare() teamName := c.GetString("teamName") if teamName == "" { c.JsonResult(5001, i18n.Tr(c.Lang, "message.team_name_empty")) } team := models.NewTeam() team.MemberId = c.Member.MemberId team.TeamName = teamName if err := team.Save(); err == nil { c.JsonResult(0, "OK", team) } else { c.JsonResult(5002, err.Error()) } } func (c *ManagerController) TeamEdit() { c.Prepare() teamName := c.GetString("teamName") teamId, _ := c.GetInt("teamId") if teamName == "" { c.JsonResult(5001, i18n.Tr(c.Lang, "message.team_name_empty")) } if teamId <= 0 { c.JsonResult(5002, i18n.Tr(c.Lang, "message.team_id_empty")) } team, err := models.NewTeam().First(teamId) c.CheckJsonError(5003, err) team.TeamName = teamName err = team.Save() c.CheckJsonError(5004, err) c.JsonResult(0, "OK", team) } func (c *ManagerController) TeamDelete() { c.Prepare() teamId, _ := c.GetInt("teamId") if teamId <= 0 { c.JsonResult(5002, i18n.Tr(c.Lang, "message.team_id_empty")) } err := models.NewTeam().Delete(teamId) c.CheckJsonError(5001, err) c.JsonResult(0, "OK") } func (c *ManagerController) TeamMemberList() { c.Prepare() c.TplName = "manager/team_member_list.tpl" c.Data["Action"] = "team" teamId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) if teamId <= 0 { c.ShowErrorPage(500, i18n.Tr(c.Lang, "message.param_error")) } pageIndex, _ := c.GetInt("page", 0) team, err := models.NewTeam().First(teamId) if err == orm.ErrNoRows { c.ShowErrorPage(404, "团队不存在") } c.CheckErrorResult(500, err) c.Data["Model"] = team teams, totalCount, err := models.NewTeamMember().SetLang(c.Lang).FindToPager(teamId, pageIndex, conf.PageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if err == orm.ErrNoRows || len(teams) <= 0 { c.Data["Result"] = template.JS("[]") c.Data["PageHtml"] = "" return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(teams) if err != nil { logs.Error("编码 JSON 结果失败 ->", err) c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } // 搜索团队用户. func (c *ManagerController) TeamSearchMember() { c.Prepare() teamId, _ := c.GetInt("teamId") keyword := strings.TrimSpace(c.GetString("q")) if teamId <= 0 { c.JsonResult(500, i18n.Tr(c.Lang, "message.param_error")) } searchResult, err := models.NewTeamMember().FindNotJoinMemberByAccount(teamId, keyword, 10) if err != nil { c.JsonResult(500, err.Error()) } c.JsonResult(0, "OK", searchResult) } func (c *ManagerController) TeamMemberAdd() { c.Prepare() teamId, _ := c.GetInt("teamId") memberId, _ := c.GetInt("memberId") roleId, _ := c.GetInt("roleId") if teamId <= 0 || memberId <= 0 || roleId <= 0 || roleId > int(conf.BookObserver) { c.JsonResult(5001, i18n.Tr(c.Lang, "message.system_error")) } teamMember := models.NewTeamMember() teamMember.MemberId = memberId teamMember.TeamId = teamId teamMember.RoleId = conf.BookRole(roleId) if err := teamMember.Save(); err != nil { c.CheckJsonError(5001, err) } teamMember.Include() c.JsonResult(0, "OK", teamMember) } func (c *ManagerController) TeamMemberDelete() { c.Prepare() memberId, _ := c.GetInt("memberId") teamId, _ := c.GetInt("teamId") teamMember, err := models.NewTeamMember().FindFirst(teamId, memberId) if err != nil { c.JsonResult(5001, "用户不存在或已禁用") } err = teamMember.Delete(teamMember.TeamMemberId) if err != nil { c.JsonResult(5002, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "ok") } func (c *ManagerController) TeamChangeMemberRole() { c.Prepare() memberId, _ := c.GetInt("memberId") roleId, _ := c.GetInt("roleId") teamId, _ := c.GetInt("teamId") if memberId <= 0 || roleId <= 0 || teamId <= 0 || roleId > int(conf.BookObserver) { c.JsonResult(5001, i18n.Tr(c.Lang, "message.param_error")) } teamMember, err := models.NewTeamMember().ChangeRoleId(teamId, memberId, conf.BookRole(roleId)) if err != nil { c.JsonResult(5002, err.Error()) } else { teamMember.SetLang(c.Lang).Include() c.JsonResult(0, "OK", teamMember) } } // 团队项目列表. func (c *ManagerController) TeamBookList() { c.Prepare() c.TplName = "manager/team_book_list.tpl" c.Data["Action"] = "team" teamId, _ := strconv.Atoi(c.Ctx.Input.Param(":id")) pageIndex, _ := c.GetInt("page", 0) if teamId <= 0 { c.JsonResult(5002, i18n.Tr(c.Lang, "message.team_id_empty")) } team, err := models.NewTeam().First(teamId) if err == orm.ErrNoRows { c.ShowErrorPage(404, "团队不存在") } c.CheckErrorResult(500, err) c.Data["Model"] = team teams, totalCount, err := models.NewTeamRelationship().FindToPager(teamId, pageIndex, conf.PageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if err == orm.ErrNoRows || len(teams) <= 0 { c.Data["Result"] = template.JS("[]") c.Data["PageHtml"] = "" return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } b, err := json.Marshal(teams) if err != nil { logs.Error("编码 JSON 结果失败 ->", err) c.Data["Result"] = template.JS("[]") } else { c.Data["Result"] = template.JS(string(b)) } } // 给团队增加项目. func (c *ManagerController) TeamBookAdd() { c.Prepare() teamId, _ := c.GetInt("teamId") bookId, _ := c.GetInt("bookId") if teamId <= 0 || bookId <= 0 { c.JsonResult(500, i18n.Tr(c.Lang, "message.param_error")) } teamRel := models.NewTeamRelationship() teamRel.BookId = bookId teamRel.TeamId = teamId err := teamRel.Save() if err != nil { c.JsonResult(5001, err.Error()) } else { teamRel.Include() c.JsonResult(0, "OK", teamRel) } } // 搜索未参与的项目. func (c *ManagerController) TeamSearchBook() { c.Prepare() teamId, _ := c.GetInt("teamId") keyword := strings.TrimSpace(c.GetString("q")) if teamId <= 0 { c.JsonResult(500, i18n.Tr(c.Lang, "message.param_error")) } searchResult, err := models.NewTeamRelationship().FindNotJoinBookByName(teamId, keyword, 10) if err != nil { c.JsonResult(500, err.Error()) } c.JsonResult(0, "OK", searchResult) } // 删除团队项目. func (c *ManagerController) TeamBookDelete() { c.Prepare() teamRelationshipId, _ := c.GetInt("teamRelId") if teamRelationshipId <= 0 { c.JsonResult(500, i18n.Tr(c.Lang, "message.param_error")) } err := models.NewTeamRelationship().Delete(teamRelationshipId) if err != nil { c.JsonResult(5001, i18n.Tr(c.Lang, "message.failed")) } c.JsonResult(0, "OK") } // 项目空间列表. func (c *ManagerController) Itemsets() { c.Prepare() c.TplName = "manager/itemsets.tpl" c.Data["Action"] = "itemsets" pageIndex, _ := c.GetInt("page", 0) items, totalCount, err := models.NewItemsets().FindToPager(pageIndex, conf.PageSize) if err != nil && err != orm.ErrNoRows { c.ShowErrorPage(500, err.Error()) } if err == orm.ErrNoRows || len(items) <= 0 { c.Data["Lists"] = items c.Data["PageHtml"] = "" return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } c.Data["Lists"] = items } // 编辑或添加项目空间. func (c *ManagerController) ItemsetsEdit() { c.Prepare() itemId, _ := c.GetInt("itemId") itemName := strings.TrimSpace(c.GetString("itemName")) itemKey := strings.TrimSpace(c.GetString("itemKey")) if itemName == "" || itemKey == "" { c.JsonResult(5001, i18n.Tr(c.Lang, "message.param_error")) } var item *models.Itemsets var err error if itemId > 0 { if item, err = models.NewItemsets().First(itemId); err != nil { if err == orm.ErrNoRows { c.JsonResult(5002, i18n.Tr(c.Lang, "message.project_space_not_exist")) } else { c.JsonResult(5003, "查询项目空间出错") } } } else { item = models.NewItemsets() } item.ItemKey = itemKey item.ItemName = itemName item.MemberId = c.Member.MemberId item.ModifyAt = c.Member.MemberId if err := item.Save(); err != nil { c.JsonResult(5004, err.Error()) } c.JsonResult(0, "OK") } // 删除项目空间. func (c *ManagerController) ItemsetsDelete() { c.Prepare() itemId, _ := c.GetInt("itemId") if err := models.NewItemsets().Delete(itemId); err != nil { c.JsonResult(5001, err.Error()) } c.JsonResult(0, "OK") } ================================================ FILE: controllers/SearchController.go ================================================ package controllers import ( "strconv" "strings" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/pagination" "github.com/mindoc-org/mindoc/utils/segmenter" "github.com/mindoc-org/mindoc/utils/sqltil" ) // SearchV2Result 用于 IndexV2 的搜索结果结构 type SearchV2Result struct { SearchType string `json:"search_type"` DocumentId int `json:"doc_id"` DocumentName string `json:"doc_name"` Identify string `json:"identify"` Description string `json:"description"` Author string `json:"author"` ModifyTime interface{} `json:"modify_time"` CreateTime interface{} `json:"create_time"` BookId int `json:"book_id"` BookName string `json:"book_name"` BookIdentify string `json:"book_identify"` } // SearchV2RawResult 底层搜索返回的原始结果,包含倒排索引的分数信息 type SearchV2RawResult struct { ContentType int // 1-Document 2-Blog ContentId int // 文档ID或博客ID Score float64 // TF-IDF分数 WordCounts []int // 各个词的词频 SearchType string // "document" 或 "blog" DocumentId int // 文档ID DocumentName string // 文档名称/博客标题 Identify string // 文档标识/博客标识 Description string // 描述 Content string // 原始内容 Author string // 作者 ModifyTime interface{} // 修改时间 CreateTime interface{} // 创建时间 BookId int // 项目ID (仅Document) BookName string // 项目名称 (仅Document) BookIdentify string // 项目标识 (仅Document) BlogId int // 博客ID (仅Blog) BlogTitle string // 博客标题 (仅Blog) BlogIdentify string // 博客标识 (仅Blog) BlogExcerpt string // 博客摘要 (仅Blog) } // PerformSearchV2Raw 执行倒排索引搜索的底层函数,返回原始结果 func PerformSearchV2Raw(keyword string, pageIndex, pageSize int, memberId int) ([]*SearchV2RawResult, []string, int, error) { // 使用分词器对关键词进行分词 words := segmenter.Segment(keyword) if len(words) == 0 { // 如果分词结果为空,直接使用原关键词 words = []string{keyword} } // 使用倒排索引模型进行搜索 index := models.NewContentReverseIndex() results, totalCount, err := index.FindByWordsWithPagination(words, pageIndex, pageSize) if err != nil { return nil, words, 0, err } // 构建返回结果 searchResults := make([]*SearchV2RawResult, 0) for _, result := range results { item := &SearchV2RawResult{ ContentType: result.ContentType, ContentId: result.ContentId, Score: result.Score, WordCounts: result.WordCounts, } // 根据内容类型获取详细信息 if result.ContentType == 1 { // Document类型 doc, err := models.NewDocument().Find(result.ContentId) if err == nil { // 检查文档权限 book, bookErr := models.NewBook().Find(doc.BookId) if bookErr != nil { continue } item.SearchType = "document" item.DocumentId = doc.DocumentId item.DocumentName = doc.DocumentName item.BookId = doc.BookId item.BookName = book.BookName item.Identify = doc.Identify item.BookIdentify = book.Identify item.CreateTime = doc.CreateTime item.ModifyTime = doc.ModifyTime item.Content = doc.Release // 获取作者信息 if doc.MemberId > 0 { member, _ := models.NewMember().Find(doc.MemberId, "real_name", "account") if member != nil { if member.RealName != "" { item.Author = member.RealName } else { item.Author = member.Account } } } // 提取描述 description := doc.Release if description == "" { description = doc.Markdown } // 去除HTML标签 description = utils.StripTags(description) if len([]rune(description)) > 100 { description = string([]rune(description)[:100]) + "..." } item.Description = description searchResults = append(searchResults, item) } } else if result.ContentType == 2 { // Blog类型 blog, err := models.NewBlog().Find(result.ContentId) if err == nil { item.SearchType = "blog" item.BlogId = blog.BlogId item.BlogTitle = blog.BlogTitle item.DocumentId = blog.BlogId item.DocumentName = blog.BlogTitle item.BlogIdentify = blog.BlogIdentify item.Identify = blog.BlogIdentify item.BlogExcerpt = blog.BlogExcerpt item.CreateTime = blog.Created item.ModifyTime = blog.Modified item.Content = blog.BlogRelease // 获取作者信息 if blog.MemberId > 0 { member, _ := models.NewMember().Find(blog.MemberId, "real_name", "account") if member != nil { if member.RealName != "" { item.Author = member.RealName } else { item.Author = member.Account } } } // 提取描述 description := blog.BlogExcerpt if description == "" { description = blog.BlogRelease if description == "" { description = blog.BlogContent } } description = utils.StripTags(description) if len([]rune(description)) > 100 { description = string([]rune(description)[:100]) + "..." } item.Description = description searchResults = append(searchResults, item) } } } return searchResults, words, totalCount, nil } // performSearchV2 执行倒排索引搜索,返回 SearchV2Result 列表 func (c *SearchController) performSearchV2(keyword string, pageIndex, pageSize int) ([]*SearchV2Result, int, error) { memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } rawResults, words, totalCount, err := PerformSearchV2Raw(keyword, pageIndex, pageSize, memberId) if err != nil { return nil, 0, err } // 转换为 SearchV2Result searchResults := make([]*SearchV2Result, 0, len(rawResults)) for _, raw := range rawResults { item := &SearchV2Result{ SearchType: raw.SearchType, DocumentId: raw.DocumentId, DocumentName: raw.DocumentName, Identify: raw.Identify, Description: raw.Description, Author: raw.Author, ModifyTime: raw.ModifyTime, CreateTime: raw.CreateTime, BookId: raw.BookId, BookName: raw.BookName, BookIdentify: raw.BookIdentify, } searchResults = append(searchResults, item) } // 高亮关键词 for _, item := range searchResults { for _, word := range words { if word != "" { item.DocumentName = strings.Replace(item.DocumentName, word, ""+word+"", -1) if item.Description != "" { item.Description = strings.Replace(item.Description, word, ""+word+"", -1) } } } } return searchResults, totalCount, nil } type SearchController struct { BaseController } // 搜索首页 func (c *SearchController) Index() { c.Prepare() c.TplName = "search/index.tpl" //如果没有开启你们访问则跳转到登录 if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login"), 302) return } keyword := c.GetString("keyword") pageIndex, _ := c.GetInt("page", 1) c.Data["BaseUrl"] = c.BaseUrl() if keyword != "" { c.Data["Keyword"] = keyword memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } searchResult, totalCount, err := models.NewDocumentSearchResult().FindToPager(sqltil.EscapeLike(keyword), pageIndex, conf.PageSize, memberId) if err != nil { logs.Error("搜索失败 ->", err) return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } if len(searchResult) > 0 { keywords := strings.Split(keyword, " ") for _, item := range searchResult { for _, word := range keywords { item.DocumentName = strings.Replace(item.DocumentName, word, ""+word+"", -1) if item.Description != "" { src := item.Description r := []rune(utils.StripTags(item.Description)) if len(r) > 100 { src = string(r[:100]) } else { src = string(r) } item.Description = strings.Replace(src, word, ""+word+"", -1) } } if item.Identify == "" { item.Identify = strconv.Itoa(item.DocumentId) } if item.ModifyTime.IsZero() { item.ModifyTime = item.CreateTime } } } c.Data["Lists"] = searchResult } } // 搜索用户 func (c *SearchController) User() { c.Prepare() key := c.Ctx.Input.Param(":key") keyword := strings.TrimSpace(c.GetString("q")) if key == "" || keyword == "" { c.JsonResult(404, i18n.Tr(c.Lang, "message.param_error")) } keyword = sqltil.EscapeLike(keyword) book, err := models.NewBookResult().FindByIdentify(key, c.Member.MemberId) if err != nil { if err == models.ErrPermissionDenied { c.JsonResult(403, i18n.Tr(c.Lang, "message.no_permission")) } c.JsonResult(500, i18n.Tr(c.Lang, "message.item_not_exist")) } //members, err := models.NewMemberRelationshipResult().FindNotJoinUsersByAccount(book.BookId, 10, "%"+keyword+"%") members, err := models.NewMemberRelationshipResult().FindNotJoinUsersByAccountOrRealName(book.BookId, 10, "%"+keyword+"%") if err != nil { logs.Error("查询用户列表出错:" + err.Error()) c.JsonResult(500, err.Error()) } result := models.SelectMemberResult{} items := make([]models.KeyValueItem, 0) for _, member := range members { item := models.KeyValueItem{} item.Id = member.MemberId item.Text = member.Account + "[" + member.RealName + "]" items = append(items, item) } result.Result = items c.JsonResult(0, "OK", result) } // IndexV2 使用倒排索引的搜索页面 func (c *SearchController) IndexV2() { c.Prepare() c.TplName = "search/index.tpl" // 如果没有开启你们访问则跳转到登录 if !c.EnableAnonymous && c.Member == nil { c.Redirect(conf.URLFor("AccountController.Login"), 302) return } keyword := strings.TrimSpace(c.GetString("keyword")) pageIndex, _ := c.GetInt("page", 1) pageSize := conf.PageSize c.Data["BaseUrl"] = c.BaseUrl() if keyword != "" { c.Data["Keyword"] = keyword searchResult, totalCount, err := c.performSearchV2(keyword, pageIndex, pageSize) if err != nil { logs.Error("搜索失败 ->", err) return } if totalCount > 0 { pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl()) c.Data["PageHtml"] = pager.HtmlPages() } else { c.Data["PageHtml"] = "" } // 处理结果中的一些字段 for _, item := range searchResult { if item.Identify == "" { item.Identify = strconv.Itoa(item.DocumentId) } } c.Data["Lists"] = searchResult } } // SearchV2 使用倒排索引进行搜索 func (c *SearchController) SearchV2() { c.Prepare() memberId := 0 if c.Member != nil { memberId = c.Member.MemberId } // 如果没有开启匿名访问且未登录则返回错误 if !c.EnableAnonymous && c.Member == nil { c.JsonResult(401, "请先登录") return } keyword := strings.TrimSpace(c.GetString("keyword")) if keyword == "" { c.JsonResult(400, "搜索关键词不能为空") return } pageIndex, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("page_size", 10) // 使用底层搜索函数 rawResults, words, totalCount, err := PerformSearchV2Raw(keyword, pageIndex, pageSize, memberId) if err != nil { logs.Error("倒排索引搜索失败 ->", err) c.JsonResult(500, "搜索失败") return } // 构建返回结果 searchResults := make([]map[string]interface{}, 0) for _, raw := range rawResults { item := make(map[string]interface{}) item["content_type"] = raw.ContentType item["content_id"] = raw.ContentId item["score"] = raw.Score item["word_counts"] = raw.WordCounts item["content"] = raw.Content if raw.ContentType == 1 { // Document类型 item["type"] = "document" item["document_id"] = raw.DocumentId item["document_name"] = raw.DocumentName item["book_id"] = raw.BookId item["book_name"] = raw.BookName item["identify"] = raw.Identify item["book_identify"] = raw.BookIdentify item["create_time"] = raw.CreateTime item["modify_time"] = raw.ModifyTime item["description"] = raw.Description } else if raw.ContentType == 2 { // Blog类型 item["type"] = "blog" item["blog_id"] = raw.BlogId item["blog_title"] = raw.BlogTitle item["blog_identify"] = raw.BlogIdentify item["blog_excerpt"] = raw.BlogExcerpt item["create_time"] = raw.CreateTime item["modify_time"] = raw.ModifyTime } searchResults = append(searchResults, item) } // 高亮关键词 for _, item := range searchResults { for _, word := range words { if word != "" { if title, ok := item["document_name"]; ok { item["document_name"] = strings.Replace(title.(string), word, ""+word+"", -1) } if title, ok := item["blog_title"]; ok { item["blog_title"] = strings.Replace(title.(string), word, ""+word+"", -1) } if desc, ok := item["description"]; ok { item["description"] = strings.Replace(desc.(string), word, ""+word+"", -1) } if excerpt, ok := item["blog_excerpt"]; ok { item["blog_excerpt"] = strings.Replace(excerpt.(string), word, ""+word+"", -1) } } } } responseData := map[string]interface{}{ "lists": searchResults, "total": totalCount, "page": pageIndex, "page_size": pageSize, } c.JsonResult(0, "OK", responseData) } ================================================ FILE: controllers/SettingController.go ================================================ package controllers import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/graphics" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" ) type SettingController struct { BaseController } func (c *SettingController) Index() { c.TplName = "setting/index.tpl" if c.Ctx.Input.IsPost() { email := strings.TrimSpace(c.GetString("email", "")) phone := strings.TrimSpace(c.GetString("phone")) description := strings.TrimSpace(c.GetString("description")) if email == "" { c.JsonResult(601, i18n.Tr(c.Lang, "message.email_empty")) } member := c.Member member.Email = email member.Phone = phone member.Description = description member.RealName = strings.TrimSpace(c.GetString("real_name", "")) if err := member.Update(); err != nil { c.JsonResult(602, err.Error()) } c.SetMember(*member) c.JsonResult(0, "ok") } } func (c *SettingController) Password() { c.TplName = "setting/password.tpl" if c.Ctx.Input.IsPost() { if c.Member.AuthMethod == conf.AuthMethodLDAP { c.JsonResult(6009, i18n.Tr(c.Lang, "message.cur_user_cannot_change_pwd")) } password1 := c.GetString("password1") password2 := c.GetString("password2") password3 := c.GetString("password3") if password1 == "" { c.JsonResult(6003, i18n.Tr(c.Lang, "message.origin_pwd_empty")) } if password2 == "" { c.JsonResult(6004, i18n.Tr(c.Lang, "message.new_pwd_empty")) } if count := strings.Count(password2, ""); count < 6 || count > 18 { c.JsonResult(6009, i18n.Tr(c.Lang, "message.pwd_length")) } if password2 != password3 { c.JsonResult(6003, "确认密码不正确") } if ok, _ := utils.PasswordVerify(c.Member.Password, password1); !ok { c.JsonResult(6005, i18n.Tr(c.Lang, "message.wrong_origin_pwd")) } if password1 == password2 { c.JsonResult(6006, i18n.Tr(c.Lang, "message.same_pwd")) } pwd, err := utils.PasswordHash(password2) if err != nil { c.JsonResult(6007, i18n.Tr(c.Lang, "message.pwd_encrypt_failed")) } c.Member.Password = pwd if c.Member.AuthMethod == "" { c.Member.AuthMethod = "local" } if err := c.Member.Update(); err != nil { c.JsonResult(6008, err.Error()) } c.JsonResult(0, "ok") } } // Upload 上传图片 func (c *SettingController) Upload() { file, moreFile, err := c.GetFile("image-file") defer file.Close() if err != nil { logs.Error("", err.Error()) c.JsonResult(500, "读取文件异常") } ext := filepath.Ext(moreFile.Filename) if !strings.EqualFold(ext, ".png") && !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".gif") && !strings.EqualFold(ext, ".jpeg") { c.JsonResult(500, "不支持的图片格式") } x1, _ := strconv.ParseFloat(c.GetString("x"), 10) y1, _ := strconv.ParseFloat(c.GetString("y"), 10) w1, _ := strconv.ParseFloat(c.GetString("width"), 10) h1, _ := strconv.ParseFloat(c.GetString("height"), 10) x := int(x1) y := int(y1) width := int(w1) height := int(h1) fmt.Println(x, x1, y, y1) fileName := "avatar_" + strconv.FormatInt(time.Now().UnixNano(), 16) filePath := filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), fileName+ext) path := filepath.Dir(filePath) os.MkdirAll(path, os.ModePerm) err = c.SaveToFile("image-file", filePath) if err != nil { logs.Error("", err) c.JsonResult(500, "图片保存失败") } //剪切图片 subImg, err := graphics.ImageCopyFromFile(filePath, x, y, width, height) if err != nil { logs.Error("ImageCopyFromFile => ", err) c.JsonResult(6001, "头像剪切失败") } os.Remove(filePath) filePath = filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), fileName+"_small"+ext) err = graphics.ImageResizeSaveFile(subImg, 120, 120, filePath) //err = graphics.SaveImage(filePath,subImg) if err != nil { logs.Error("保存文件失败 => ", err.Error()) c.JsonResult(500, "保存文件失败") } url := "/" + strings.Replace(strings.TrimPrefix(filePath, conf.WorkingDirectory), "\\", "/", -1) if strings.HasPrefix(url, "//") { url = string(url[1:]) } if member, err := models.NewMember().Find(c.Member.MemberId); err == nil { avater := member.Avatar member.Avatar = url err := member.Update() if err == nil { if strings.HasPrefix(avater, "/uploads/") { os.Remove(filepath.Join(conf.WorkingDirectory, avater)) } c.SetMember(*member) } else { c.JsonResult(60001, "保存头像失败") } } c.JsonResult(0, "ok", url) } ================================================ FILE: controllers/TemplateController.go ================================================ package controllers import ( "errors" "strings" "github.com/beego/beego/v2/client/orm" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/models" ) type TemplateController struct { BaseController BookId int } func (c *TemplateController) isPermission() error { c.Prepare() bookIdentify := c.GetString("identify", "") if bookIdentify == "" { return errors.New(i18n.Tr(c.Lang, "message.param_error")) } if !c.Member.IsAdministrator() { book, err := models.NewBookResult().FindByIdentify(bookIdentify, c.Member.MemberId) if err != nil { if err == orm.ErrNoRows { return errors.New("项目不存在或没有权限") } return errors.New("查询项目模板失败") } c.BookId = book.BookId } else { book, err := models.NewBook().FindByIdentify(bookIdentify, "book_id") if err != nil { if err == orm.ErrNoRows { return errors.New("项目不存在或没有权限") } return errors.New("查询项目模板失败") } c.BookId = book.BookId } return nil } //获取指定模板信息 func (c *TemplateController) Get() { if err := c.isPermission(); err != nil { c.JsonResult(500, err.Error()) } templateId, _ := c.GetInt("template_id", 0) template, err := models.NewTemplate().Find(templateId) if err != nil { c.JsonResult(500, "读取模板失败") } if template.IsGlobal == 0 && template.BookId != c.BookId { c.JsonResult(404, "模板不存在或已删除") } c.JsonResult(0, "OK", template) } //获取模板列表 func (c *TemplateController) List() { c.TplName = "template/list.tpl" if err := c.isPermission(); err != nil { c.Data["ErrorMessage"] = err.Error() return } templateList, err := models.NewTemplate().FindAllByBookId(c.BookId) if err != nil && err != orm.ErrNoRows { c.Data["ErrorMessage"] = "查询项目模板失败" } if templateList != nil { for i, t := range templateList { templateList[i] = t.Preload() } } //c.JsonResult(0,"OK",templateList) c.Data["List"] = templateList } //添加模板 func (c *TemplateController) Add() { if err := c.isPermission(); err != nil { c.JsonResult(500, err.Error()) } templateId, _ := c.GetInt("template_id", 0) content := c.GetString("content") isGlobal, _ := c.GetInt("is_global", 0) templateName := c.GetString("template_name", "") if templateName == "" || strings.Count(templateName, "") > 300 { c.JsonResult(500, "模板名称不能为空且必须小于300字") } template := models.NewTemplate() if templateId > 0 { t, err := template.Find(templateId) if err != nil { c.JsonResult(500, "模板不存在") } template = t template.ModifyAt = c.Member.MemberId } template.TemplateId = templateId template.BookId = c.BookId template.TemplateContent = content template.TemplateName = templateName //只有管理员才能设置全局模板 if c.Member.IsAdministrator() { template.IsGlobal = isGlobal } else { template.IsGlobal = 0 //如果不是管理员需要判断是否有项目权限 rel, err := models.NewRelationship().FindByBookIdAndMemberId(c.BookId, c.Member.MemberId) if err != nil || rel.RoleId == conf.BookObserver { c.JsonResult(403, "没有权限") } //如果修改的模板不是本人创建的,并且又不是项目创建者则禁止修改 if template.MemberId > 0 && template.MemberId != c.Member.MemberId && rel.RoleId != conf.BookFounder { c.JsonResult(403, "没有权限") } } template.MemberId = c.Member.MemberId var cols []string if templateId > 0 { cols = []string{"template_content", "modify_time", "modify_at", "version"} } if err := template.Save(cols...); err != nil { c.JsonResult(500, "报错模板失败") } c.JsonResult(0, "OK", template) } //删除模板 func (c *TemplateController) Delete() { if err := c.isPermission(); err != nil { c.JsonResult(500, err.Error()) } templateId, _ := c.GetInt("template_id", 0) template, err := models.NewTemplate().Find(templateId) if err != nil { c.JsonResult(404, "模板不存在") } if c.Member.IsAdministrator() { err := models.NewTemplate().Delete(templateId, 0) if err != nil { c.JsonResult(500, "删除失败") } } else { //如果不是管理员需要判断是否有项目权限 rel, err := models.NewRelationship().FindByBookIdAndMemberId(template.BookId, c.Member.MemberId) if err != nil || rel.RoleId == conf.BookObserver { c.JsonResult(403, "没有权限") } //如果是创始人或管理者可以直接删除模板 if rel.RoleId == conf.BookFounder || rel.RoleId == conf.BookAdmin { err := models.NewTemplate().Delete(templateId, 0) if err != nil { c.JsonResult(500, "删除失败") } } if err := models.NewTemplate().Delete(templateId, c.Member.MemberId); err != nil { c.JsonResult(500, "删除失败") } } c.JsonResult(0, "OK") } ================================================ FILE: controllers/const.go ================================================ package controllers const ( Markdown = "markdown" EditorMarkdown = "markdown" EditorCherryMarkdown = "cherry_markdown" EditorHtml = "html" EditorNewHtml = "new_html" EditorFroala = "froala" ) ================================================ FILE: converter/converter.go ================================================ //Author:TruthHun //Email:TruthHun@QQ.COM //Date:2018-01-21 package converter import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "errors" "os/exec" "time" "html" "sync" "github.com/mindoc-org/mindoc/utils/cryptil" "github.com/mindoc-org/mindoc/utils/filetil" "github.com/mindoc-org/mindoc/utils/ziptil" ) type Converter struct { BasePath string OutputPath string Config Config Debug bool GeneratedCover string ProcessNum int //并发的任务数量 process chan func() limitChan chan bool } //目录结构 type Toc struct { Id int `json:"id"` Link string `json:"link"` Pid int `json:"pid"` Title string `json:"title"` } //config.json文件解析结构 type Config struct { Charset string `json:"charset"` //字符编码,默认utf-8编码 Cover string `json:"cover"` //封面图片,或者封面html文件 Timestamp string `json:"date"` //时间日期,如“2018-01-01 12:12:21”,其实是time.Time格式,但是直接用string就好 Description string `json:"description"` //摘要 Footer string `json:"footer"` //pdf的footer Header string `json:"header"` //pdf的header Identifier string `json:"identifier"` //即uuid,留空即可 Language string `json:"language"` //语言,如zh、en、zh-CN、en-US等 Creator string `json:"creator"` //作者,即author Publisher string `json:"publisher"` //出版单位 Contributor string `json:"contributor"` //同Publisher Title string `json:"title"` //文档标题 Format []string `json:"format"` //导出格式,可选值:pdf、epub、mobi FontSize string `json:"font_size"` //默认的pdf导出字体大小 PaperSize string `json:"paper_size"` //页面大小 MarginLeft string `json:"margin_left"` //PDF文档左边距,写数字即可,默认72pt MarginRight string `json:"margin_right"` //PDF文档左边距,写数字即可,默认72pt MarginTop string `json:"margin_top"` //PDF文档左边距,写数字即可,默认72pt MarginBottom string `json:"margin_bottom"` //PDF文档左边距,写数字即可,默认72pt More []string `json:"more"` //更多导出选项[PDF导出选项,具体参考:https://manual.calibre-ebook.com/generated/en/ebook-convert.html#pdf-output-options] Toc []Toc `json:"toc"` //目录 /////////////////////////////////////////// Order []string `json:"-"` //这个不需要赋值 } var ( output = "output" //文档导出文件夹 ebookConvert = "ebook-convert" ) func CheckConvertCommand() error { args := []string{ "--version" } cmd := exec.Command(ebookConvert, args...) return cmd.Run() } // 接口文档 https://manual.calibre-ebook.com/generated/en/ebook-convert.html#table-of-contents //根据json配置文件,创建文档转化对象 func NewConverter(configFile string, debug ...bool) (converter *Converter, err error) { var ( cfg Config basepath string db bool ) if len(debug) > 0 { db = debug[0] } if cfg, err = parseConfig(configFile); err == nil { if basepath, err = filepath.Abs(filepath.Dir(configFile)); err == nil { //设置默认值 if len(cfg.Timestamp) == 0 { cfg.Timestamp = time.Now().Format("2006-01-02 15:04:05") } if len(cfg.Charset) == 0 { cfg.Charset = "utf-8" } converter = &Converter{ Config: cfg, BasePath: basepath, Debug: db, ProcessNum: 1, process: make(chan func(),4), limitChan: make(chan bool,1), } } } return } //执行文档转换 func (convert *Converter) Convert() (err error) { if !convert.Debug { //调试模式下不删除生成的文件 defer convert.converterDefer() //最后移除创建的多余而文件 } if convert.process == nil{ convert.process = make(chan func(),4) } if convert.limitChan == nil { if convert.ProcessNum <= 0 { convert.ProcessNum = 1 } convert.limitChan = make(chan bool,convert.ProcessNum) for i := 0; i < convert.ProcessNum;i++{ convert.limitChan <- true } } if err = convert.generateMimeType(); err != nil { return } if err = convert.generateMetaInfo(); err != nil { return } if err = convert.generateTocNcx(); err != nil { //生成目录 return } if err = convert.generateSummary(); err != nil { //生成文档内目录 return } if err = convert.generateTitlePage(); err != nil { //生成封面 return } if err = convert.generateContentOpf(); err != nil { //这个必须是generate*系列方法的最后一个调用 return } //将当前文件夹下的所有文件压缩成zip包,然后直接改名成content.epub f := filepath.Join(convert.OutputPath, "content.epub") os.Remove(f) //如果原文件存在了,则删除; if err = ziptil.Zip(convert.BasePath,f); err == nil { //创建导出文件夹 os.Mkdir(convert.BasePath+"/"+output, os.ModePerm) if len(convert.Config.Format) > 0 { var errs []string go func(convert *Converter) { for _, v := range convert.Config.Format { fmt.Println("convert to " + v) switch strings.ToLower(v) { case "epub": convert.process <- func() { if err = convert.convertToEpub(); err != nil { errs = append(errs, err.Error()) fmt.Println("转换EPUB文档失败:" + err.Error()) } } case "mobi": convert.process <- func() { if err = convert.convertToMobi(); err != nil { errs = append(errs, err.Error()) fmt.Println("转换MOBI文档失败:" + err.Error()) } } case "pdf": convert.process <- func() { if err = convert.convertToPdf(); err != nil { fmt.Println("转换PDF文档失败:" + err.Error()) errs = append(errs, err.Error()) } } case "docx": convert.process <- func() { if err = convert.convertToDocx(); err != nil { fmt.Println("转换WORD文档失败:" + err.Error()) errs = append(errs, err.Error()) } } } } close(convert.process) }(convert) group := sync.WaitGroup{} for { action, isClosed := <-convert.process if action == nil && !isClosed { break; } group.Add(1) <- convert.limitChan go func(group *sync.WaitGroup) { action() group.Done() convert.limitChan <- true }(&group) } group.Wait() if len(errs) > 0 { err = errors.New(strings.Join(errs, "\n")) } } else { err = convert.convertToPdf() if err != nil { fmt.Println(err) } } } else { fmt.Println("压缩目录出错" + err.Error()) } return } //删除生成导出文档而创建的文件 func (this *Converter) converterDefer() { //删除不必要的文件 os.RemoveAll(filepath.Join(this.BasePath, "META-INF")) os.RemoveAll(filepath.Join(this.BasePath, "content.epub")) os.RemoveAll(filepath.Join(this.BasePath, "mimetype")) os.RemoveAll(filepath.Join(this.BasePath, "toc.ncx")) os.RemoveAll(filepath.Join(this.BasePath, "content.opf")) os.RemoveAll(filepath.Join(this.BasePath, "titlepage.xhtml")) //封面图片待优化 os.RemoveAll(filepath.Join(this.BasePath, "summary.html")) //文档目录 } //生成metainfo func (this *Converter) generateMetaInfo() (err error) { xml := ` ` folder := filepath.Join(this.BasePath, "META-INF") os.MkdirAll(folder, os.ModePerm) err = ioutil.WriteFile(filepath.Join(folder, "container.xml"), []byte(xml), os.ModePerm) return } //形成mimetyppe func (this *Converter) generateMimeType() (err error) { return ioutil.WriteFile(filepath.Join(this.BasePath, "mimetype"), []byte("application/epub+zip"), os.ModePerm) } //生成封面 func (this *Converter) generateTitlePage() (err error) { if ext := strings.ToLower(filepath.Ext(this.Config.Cover)); !(ext == ".html" || ext == ".xhtml") { xml := ` Cover
` if err = ioutil.WriteFile(filepath.Join(this.BasePath, "titlepage.xhtml"), []byte(xml), os.ModePerm); err == nil { this.GeneratedCover = "titlepage.xhtml" } } return } //生成文档目录 func (this *Converter) generateTocNcx() (err error) { ncx := ` %v %v ` codes, _ := this.tocToXml(0, 1) ncx = fmt.Sprintf(ncx, this.Config.Language, html.EscapeString(this.Config.Title), strings.Join(codes, "")) return ioutil.WriteFile(filepath.Join(this.BasePath, "toc.ncx"), []byte(ncx), os.ModePerm) } //生成文档目录,即summary.html func (this *Converter) generateSummary() (err error) { //目录 summary := ` 目录

目    录

%v ` summary = fmt.Sprintf(summary, strings.Join(this.tocToSummary(0), "")) return ioutil.WriteFile(filepath.Join(this.BasePath, "summary.html"), []byte(summary), os.ModePerm) } //将toc转成toc.ncx文件 func (this *Converter) tocToXml(pid, idx int) (codes []string, next_idx int) { var code string for _, toc := range this.Config.Toc { if toc.Pid == pid { code, idx = this.getNavPoint(toc, idx) codes = append(codes, code) for _, item := range this.Config.Toc { if item.Pid == toc.Id { code, idx = this.getNavPoint(item, idx) codes = append(codes, code) var code_arr []string code_arr, idx = this.tocToXml(item.Id, idx) codes = append(codes, code_arr...) codes = append(codes, ``) } } codes = append(codes, ``) } } next_idx = idx return } //将toc转成toc.ncx文件 func (this *Converter) tocToSummary(pid int) (summarys []string) { summarys = append(summarys, "") return } //生成navPoint func (this *Converter) getNavPoint(toc Toc, idx int) (navpoint string, nextidx int) { navpoint = ` %v ` navpoint = fmt.Sprintf(navpoint, toc.Id, idx, html.EscapeString(toc.Title), toc.Link) this.Config.Order = append(this.Config.Order, toc.Link) nextidx = idx + 1 return } //生成content.opf文件 //倒数第二步调用 func (this *Converter) generateContentOpf() (err error) { var ( guide string manifest string manifestArr []string spine string //注意:如果存在封面,则需要把封面放在第一个位置 spineArr []string ) meta := `%v %v %v %v %v %v ` meta = fmt.Sprintf(meta, html.EscapeString(this.Config.Title), html.EscapeString(this.Config.Contributor), html.EscapeString(this.Config.Publisher), html.EscapeString(this.Config.Description), this.Config.Language, html.EscapeString(this.Config.Creator), this.Config.Timestamp) if len(this.Config.Cover) > 0 { meta = meta + `` guide = `` manifest = fmt.Sprintf(``, this.Config.Cover, GetMediaType(filepath.Ext(this.Config.Cover))) spineArr = append(spineArr, ``) } if _, err := os.Stat(this.BasePath + "/summary.html"); err == nil { spineArr = append(spineArr, ``) //目录 } //扫描所有文件 if files, err := filetil.ScanFiles(this.BasePath); err == nil { basePath := strings.Replace(this.BasePath, "\\", "/", -1) for _, file := range files { if !file.IsDir { ext := strings.ToLower(filepath.Ext(file.Path)) sourcefile := strings.TrimPrefix(file.Path, basePath+"/") id := "ncx" if ext != ".ncx" { if file.Name == "titlepage.xhtml" { //封面 id = "titlepage" } else if file.Name == "summary.html" { //目录 id = "summary" } else { id = cryptil.Md5Crypt(sourcefile) } } if mt := GetMediaType(ext); mt != "" { //不是封面图片,且media-type不为空 if sourcefile != strings.TrimLeft(this.Config.Cover, "./") { //不是封面图片,则追加进来。封面图片前面已经追加进来了 manifestArr = append(manifestArr, fmt.Sprintf(``, sourcefile, id, mt)) } } } else { fmt.Println(file.Path) } } items := make(map[string]string) for _, link := range this.Config.Order { id := cryptil.Md5Crypt(link) if _, ok := items[id]; !ok { //去重 items[id] = id spineArr = append(spineArr, fmt.Sprintf(``, id)) } } manifest = manifest + strings.Join(manifestArr, "\n") spine = strings.Join(spineArr, "\n") } else { return err } pkg := ` %v %v %v %v ` if len(guide) > 0 { guide = `` + guide + `` } pkg = fmt.Sprintf(pkg, meta, manifest, spine, guide) return ioutil.WriteFile(filepath.Join(this.BasePath, "content.opf"), []byte(pkg), os.ModePerm) } //转成epub func (this *Converter) convertToEpub() (err error) { args := []string{ filepath.Join(this.OutputPath, "content.epub"), filepath.Join(this.OutputPath, output, "book.epub"), } //cmd := exec.Command(ebookConvert, args...) // //if this.Debug { // fmt.Println(cmd.Args) //} //fmt.Println("正在转换EPUB文件", args[0]) //return cmd.Run() return filetil.CopyFile(args[0],args[1]) } //转成mobi func (this *Converter) convertToMobi() (err error) { args := []string{ filepath.Join(this.OutputPath, "content.epub"), filepath.Join(this.OutputPath, output, "book.mobi"), } cmd := exec.Command(ebookConvert, args...) if this.Debug { fmt.Println(cmd.Args) } fmt.Println("正在转换 MOBI 文件", args[0]) return cmd.Run() } //转成pdf func (this *Converter) convertToPdf() (err error) { args := []string{ filepath.Join(this.OutputPath, "content.epub"), filepath.Join(this.OutputPath, output, "book.pdf"), } //页面大小 if len(this.Config.PaperSize) > 0 { args = append(args, "--paper-size", this.Config.PaperSize) } //文字大小 if len(this.Config.FontSize) > 0 { args = append(args, "--pdf-default-font-size", this.Config.FontSize) } //header template if len(this.Config.Header) > 0 { args = append(args, "--pdf-header-template", this.Config.Header) } //footer template if len(this.Config.Footer) > 0 { args = append(args, "--pdf-footer-template",this.Config.Footer) } if strings.Count(this.Config.MarginLeft,"") > 0 { args = append(args, "--pdf-page-margin-left", this.Config.MarginLeft) } if strings.Count(this.Config.MarginTop,"") > 0 { args = append(args, "--pdf-page-margin-top", this.Config.MarginTop) } if strings.Count(this.Config.MarginRight,"") > 0 { args = append(args, "--pdf-page-margin-right", this.Config.MarginRight) } if strings.Count(this.Config.MarginBottom,"") > 0 { args = append(args, "--pdf-page-margin-bottom", this.Config.MarginBottom) } //更多选项 if len(this.Config.More) > 0 { args = append(args, this.Config.More...) } cmd := exec.Command(ebookConvert, args...) if this.Debug { fmt.Println(cmd.Args) } fmt.Println("正在转换 PDF 文件", args[0]) return cmd.Run() } // 转成word func (this *Converter) convertToDocx() (err error) { args := []string{ filepath.Join(this.OutputPath , "content.epub"), filepath.Join(this.OutputPath , output , "book.docx"), } args = append(args, "--docx-no-toc") //页面大小 if len(this.Config.PaperSize) > 0 { args = append(args, "--docx-page-size", this.Config.PaperSize) } if len(this.Config.MarginLeft) > 0 { args = append(args, "--docx-page-margin-left", this.Config.MarginLeft) } if len(this.Config.MarginTop) > 0 { args = append(args, "--docx-page-margin-top", this.Config.MarginTop) } if len(this.Config.MarginRight) > 0 { args = append(args, "--docx-page-margin-right", this.Config.MarginRight) } if len(this.Config.MarginBottom) > 0 { args = append(args, "--docx-page-margin-bottom", this.Config.MarginBottom) } cmd := exec.Command(ebookConvert, args...) if this.Debug { fmt.Println(cmd.Args) } fmt.Println("正在转换 DOCX 文件", args[0]) return cmd.Run() } ================================================ FILE: converter/util.go ================================================ //Author:TruthHun //Email:TruthHun@QQ.COM //Date:2018-01-21 package converter import ( "encoding/json" "io/ioutil" "strings" ) //media-type var MediaType = map[string]string{ ".jpeg": "image/jpeg", ".png": "image/png", ".jpg": "image/jpeg", ".gif": "image/gif", ".ico": "image/x-icon", ".bmp": "image/bmp", ".html": "application/xhtml+xml", ".xhtml": "application/xhtml+xml", ".htm": "application/xhtml+xml", ".otf": "application/x-font-opentype", ".ttf": "application/x-font-ttf", ".js": "application/x-javascript", ".ncx": "x-dtbncx+xml", ".txt": "text/plain", ".xml": "text/xml", ".css": "text/css", } //根据文件扩展名,获取media-type func GetMediaType(ext string) string { if mt, ok := MediaType[strings.ToLower(ext)]; ok { return mt } return "" } //解析配置文件 func parseConfig(configFile string) (cfg Config, err error) { var b []byte if b, err = ioutil.ReadFile(configFile); err == nil { err = json.Unmarshal(b, &cfg) } return } ================================================ FILE: database/clean.py ================================================ import sqlite3 import os, glob conn = sqlite3.connect("mindoc.db") cur = conn.cursor() #通过建立数据库游标对象,准备读写操作 cmd = """ SELECT att.http_path FROM md_attachment AS att WHERE (att.document_id != 0 OR (NOT EXISTS( SELECT 1 FROM md_documents WHERE markdown LIKE ("%" || att.http_path || "%")))) AND (att.document_id = 0 OR (NOT EXISTS( SELECT 1 FROM md_documents WHERE att.document_id = document_id ))) """ cur.execute(cmd) file_list = cur.fetchall() for file_item in file_list: item_path = file_item[0] # 1. 删除os文件 if os.path.exists(os.path.join("..", item_path[1:])): os.remove(os.path.join("..", item_path[1:])) # 2. 查询os是否删除成功,成功则删除附件记录 if not os.path.exists(os.path.join("..", item_path[1:])): cmd = """ delete from md_attachment WHERE http_path = '{}' """.format(item_path) cur.execute(cmd) conn.commit() #保存提交,确保数据保存成功 conn.close() #关闭与数据库的连接 ================================================ FILE: dev-win-build.cmd ================================================ go build -v -ldflags "-linkmode external -extldflags '-static' -w" -o mindoc_windows_amd64.exe main.go ================================================ FILE: docker-compose.yml ================================================ version: "3" services: mindoc: image: registry.cn-hangzhou.aliyuncs.com/mindoc-org/mindoc:v2.1 container_name: mindoc privileged: false restart: always ports: - 8181:8181 volumes: - /var/www/mindoc/conf://mindoc/conf - /var/www/mindoc/static://mindoc/static - /var/www/mindoc/views://mindoc/views - /var/www/mindoc/uploads://mindoc/uploads - /var/www/mindoc/runtime://mindoc/runtime - /var/www/mindoc/database://mindoc/database environment: - MINDOC_RUN_MODE=prod - MINDOC_DB_ADAPTER=sqlite3 - MINDOC_DB_DATABASE=./database/mindoc.db - MINDOC_CACHE=true - MINDOC_CACHE_PROVIDER=file - MINDOC_ENABLE_EXPORT=false - MINDOC_BASE_URL= - MINDOC_CDN_IMG_URL= - MINDOC_CDN_CSS_URL= - MINDOC_CDN_JS_URL= dns: - 223.5.5.5 - 223.6.6.6 ================================================ FILE: go.mod ================================================ module github.com/mindoc-org/mindoc go 1.23.0 require ( github.com/PuerkitoBio/goquery v1.10.2 github.com/beego/beego/v2 v2.1.6 github.com/beego/i18n v0.0.0-20161101132742-e9308947f407 github.com/boombuler/barcode v1.1.0 github.com/go-ldap/ldap/v3 v3.4.4 github.com/howeyc/fsnotify v0.9.0 github.com/kardianos/service v1.2.1 github.com/lib/pq v1.10.9 github.com/lifei6671/gocaptcha v0.2.0 github.com/mark3labs/mcp-go v0.45.0 github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-sqlite3 v1.14.34 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/russross/blackfriday/v2 v2.1.0 github.com/yanyiwu/gojieba v1.4.7 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/Unknwon/goconfig v1.0.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-redis/redis/v7 v7.4.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rivo/uniseg v0.3.4 // indirect github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/image v0.22.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8= github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU= github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A= github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beego/beego/v2 v2.1.6 h1:ny2WqvtpG1gAkEqJ9PQrOz6ZcQvVBJK+dECDOd/heIM= github.com/beego/beego/v2 v2.1.6/go.mod h1:kFJvA21OjBwixXKx7BeH+Ug492Pp+h4cORHFTf1L8e0= github.com/beego/i18n v0.0.0-20161101132742-e9308947f407 h1:WtJfx5HqASTQp7HfiZldnin8KQV2futplF3duGp5PGc= github.com/beego/i18n v0.0.0-20161101132742-e9308947f407/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY= github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk= github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lifei6671/gocaptcha v0.2.0 h1:CwMjGitq5MsYtWODQhlphdl7WhDdD243y1O2d3l8yFU= github.com/lifei6671/gocaptcha v0.2.0/go.mod h1:mcUWn1eB+kHOBHLQdmWAQ83bhEGrFTnGMqRCY7sFgUc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc= github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik= github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yanyiwu/gojieba v1.4.7 h1:2YkXELcYLTE0SJetq6xv4MjpEikWga6VpFn4jIFFQ/k= github.com/yanyiwu/gojieba v1.4.7/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: graphics/copy.go ================================================ package graphics import ( "errors" "image" "os" "github.com/nfnt/resize" ) func ImageCopy(src image.Image, x, y, w, h int) (image.Image, error) { var subImg image.Image if rgbImg, ok := src.(*image.YCbCr); ok { subImg = rgbImg.SubImage(image.Rect(x, y, x+w, y+h)).(*image.YCbCr) //图片裁剪x0 y0 x1 y1 } else if rgbImg, ok := src.(*image.RGBA); ok { subImg = rgbImg.SubImage(image.Rect(x, y, x+w, y+h)).(*image.RGBA) //图片裁剪x0 y0 x1 y1 } else if rgbImg, ok := src.(*image.NRGBA); ok { subImg = rgbImg.SubImage(image.Rect(x, y, x+w, y+h)).(*image.NRGBA) //图片裁剪x0 y0 x1 y1 } else if rgbImg, ok := src.(*image.Paletted); ok { subImg = rgbImg.SubImage(image.Rect(x, y, x+w, y+h)).(*image.Paletted) //图片裁剪x0 y0 x1 y1 } else { return subImg, errors.New("图片解码失败") } return subImg, nil } func ImageCopyFromFile(p string, x, y, w, h int) (image.Image, error) { var src image.Image file, err := os.Open(p) if err != nil { return src, err } defer file.Close() src, _, err = image.Decode(file) return ImageCopy(src, x, y, w, h) } func ImageResize(src image.Image, w, h int) image.Image { return resize.Resize(uint(w), uint(h), src, resize.Lanczos3) } func ImageResizeSaveFile(src image.Image, width, height int, p string) error { dst := resize.Resize(uint(width), uint(height), src, resize.Lanczos3) return SaveImage(p, dst) } ================================================ FILE: graphics/file.go ================================================ package graphics import ( "image" "image/gif" "image/jpeg" "image/png" "os" "path/filepath" "strings" ) // 将图片保存到指定的路径 func SaveImage(p string, src image.Image) error { os.MkdirAll(filepath.Dir(p), 0666) f, err := os.OpenFile(p, os.O_SYNC|os.O_RDWR|os.O_CREATE, 0666) if err != nil { return err } defer f.Close() ext := filepath.Ext(p) if strings.EqualFold(ext, ".jpg") || strings.EqualFold(ext, ".jpeg") { err = jpeg.Encode(f, src, &jpeg.Options{Quality: 80}) } else if strings.EqualFold(ext, ".png") { err = png.Encode(f, src) } else if strings.EqualFold(ext, ".gif") { err = gif.Encode(f, src, &gif.Options{NumColors: 256}) } return err } ================================================ FILE: lib/time/README ================================================ The zoneinfo.zip archive contains time zone files compiled using the code and data maintained as part of the IANA Time Zone Database. The IANA asserts that the database is in the public domain. For more information, see http://www.iana.org/time-zones ftp://ftp.iana.org/tz/code/tz-link.htm http://tools.ietf.org/html/rfc6557 To rebuild the archive, read and run update.bash. ================================================ FILE: lib/time/update.bash ================================================ #!/bin/bash # Copyright 2012 The Go Authors. All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # This script rebuilds the time zone files using files # downloaded from the ICANN/IANA distribution. # Consult http://www.iana.org/time-zones for the latest versions. # Versions to use. CODE=2016j DATA=2016j set -e rm -rf work mkdir work cd work mkdir zoneinfo curl -O http://www.iana.org/time-zones/repository/releases/tzcode$CODE.tar.gz curl -O http://www.iana.org/time-zones/repository/releases/tzdata$DATA.tar.gz tar xzf tzcode$CODE.tar.gz tar xzf tzdata$DATA.tar.gz # Turn off 64-bit output in time zone files. # We don't need those until 2037. perl -p -i -e 's/pass <= 2/pass <= 1/' zic.c make CFLAGS=-DSTD_INSPIRED AWK=awk TZDIR=zoneinfo posix_only # America/Los_Angeles should not be bigger than 1100 bytes. # If it is, we probably failed to disable the 64-bit output, which # triples the size of the files. size=$(ls -l zoneinfo/America/Los_Angeles | awk '{print $5}') if [ $size -gt 1200 ]; then echo 'zone file too large; 64-bit edit failed?' >&2 exit 2 fi cd zoneinfo rm -f ../../zoneinfo.zip zip -0 -r ../../zoneinfo.zip * cd ../.. echo if [ "$1" == "-work" ]; then echo Left workspace behind in work/. else rm -rf work fi echo New time zone files in zoneinfo.zip. ================================================ FILE: mail/smtp.go ================================================ package mail import ( "bytes" "crypto/md5" "crypto/tls" "encoding/base64" "encoding/hex" "errors" "fmt" "io/ioutil" "log" "net/mail" "net/smtp" "path" "path/filepath" "regexp" "strconv" "strings" "github.com/beego/beego/v2/core/logs" ) var ( imageRegex = regexp.MustCompile(`(src|background)=["'](.*?)["']`) schemeRegxp = regexp.MustCompile(`^[a-zA-Z]+://`) ) // Mail will represent a formatted email type Mail struct { To []string ToName []string Subject string HTML string Text string From string Bcc []string FromName string ReplyTo string Date string Files map[string]string Headers string BaseDir string //内容中图片路径 Charset string //编码 RetReceipt string //回执地址,空白则禁用回执 } // NewMail returns a new Mail func NewMail() Mail { return Mail{} } // SMTPClient struct type SMTPClient struct { smtpAuth smtp.Auth host string port string user string secure string } // SMTPConfig 配置结构体 type SMTPConfig struct { Username string Password string Host string Port int Secure string Identity string } func (s *SMTPConfig) Address() string { if s.Port == 0 { s.Port = 25 } return s.Host + `:` + strconv.Itoa(s.Port) } func (s *SMTPConfig) Auth() smtp.Auth { var auth smtp.Auth s.Secure = strings.ToUpper(s.Secure) switch s.Secure { case "NONE": auth = unencryptedAuth{smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)} case "LOGIN": auth = LoginAuth(s.Username, s.Password) case "SSL": fallthrough default: //auth = smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host) auth = unencryptedAuth{smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)} } return auth } func NewSMTPClient(conf *SMTPConfig) SMTPClient { return SMTPClient{ smtpAuth: conf.Auth(), host: conf.Host, port: strconv.Itoa(conf.Port), user: conf.Username, secure: conf.Secure, } } // NewMail returns a new Mail func (c *SMTPClient) NewMail() Mail { return NewMail() } // Send - It can be used for generic SMTP stuff func (c *SMTPClient) Send(m Mail) error { length := 0 if len(m.Charset) == 0 { m.Charset = "utf-8" } boundary := "COSCMSBOUNDARYFORSMTPGOLIB" var message bytes.Buffer message.WriteString(fmt.Sprintf("X-SMTPAPI: %s\r\n", m.Headers)) //回执 if len(m.RetReceipt) > 0 { message.WriteString(fmt.Sprintf("Return-Receipt-To: %s\r\n", m.RetReceipt)) message.WriteString(fmt.Sprintf("Disposition-Notification-To: %s\r\n", m.RetReceipt)) } message.WriteString(fmt.Sprintf("From: %s <%s>\r\n", m.FromName, m.From)) if len(m.ReplyTo) > 0 { message.WriteString(fmt.Sprintf("Return-Path: %s\r\n", m.ReplyTo)) } length = len(m.To) if length > 0 { nameLength := len(m.ToName) if nameLength > 0 { message.WriteString(fmt.Sprintf("To: %s <%s>", m.ToName[0], m.To[0])) } else { message.WriteString(fmt.Sprintf("To: <%s>", m.To[0])) } for i := 1; i < length; i++ { if nameLength > i { message.WriteString(fmt.Sprintf(", %s <%s>", m.ToName[i], m.To[i])) } else { message.WriteString(fmt.Sprintf(", <%s>", m.To[i])) } } } length = len(m.Bcc) if length > 0 { message.WriteString(fmt.Sprintf("Bcc: <%s>", m.Bcc[0])) for i := 1; i < length; i++ { message.WriteString(fmt.Sprintf(", <%s>", m.Bcc[i])) } } message.WriteString("\r\n") message.WriteString(fmt.Sprintf("Subject: %s\r\n", m.Subject)) message.WriteString("MIME-Version: 1.0\r\n") if m.Files != nil { message.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\n--%s\r\n", boundary, boundary)) } if len(m.HTML) > 0 { //解析内容中的图片 rs := imageRegex.FindAllStringSubmatch(m.HTML, -1) var embedImages string for _, v := range rs { surl := v[2] if v2 := schemeRegxp.FindStringIndex(surl); v2 == nil { filename := path.Base(surl) directory := path.Dir(surl) if directory == "." { directory = "" } h := md5.New() h.Write([]byte(surl + "@coscms.0")) cid := hex.EncodeToString(h.Sum(nil)) if len(m.BaseDir) > 0 && !strings.HasSuffix(m.BaseDir, "/") { m.BaseDir += "/" } if len(directory) > 0 && !strings.HasSuffix(directory, "/") { directory += "/" } if str, err := m.ReadAttachment(m.BaseDir + directory + filename); err == nil { re3 := regexp.MustCompile(v[1] + `=["']` + regexp.QuoteMeta(surl) + `["']`) m.HTML = re3.ReplaceAllString(m.HTML, v[1]+`="cid:`+cid+`"`) embedImages += fmt.Sprintf("--%s\r\n", boundary) embedImages += fmt.Sprintf("Content-Type: application/octet-stream; name=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset) embedImages += fmt.Sprintf("Content-Description: %s\r\n", filename) embedImages += fmt.Sprintf("Content-Disposition: inline; filename=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset) embedImages += fmt.Sprintf("Content-Transfer-Encoding: base64\r\nContent-ID: <%s>\r\n\r\n%s\r\n\n", cid, str) } } } part := fmt.Sprintf("Content-Type: text/html\r\n\n%s\r\n\n", m.HTML) message.WriteString(part) message.WriteString(embedImages) } else { part := fmt.Sprintf("Content-Type: text/plain\r\n\n%s\r\n\n", m.Text) message.WriteString(part) } if m.Files != nil { for key, value := range m.Files { message.WriteString(fmt.Sprintf("--%s\r\n", boundary)) message.WriteString("Content-Type: application/octect-stream\r\n") message.WriteString("Content-Transfer-Encoding:base64\r\n") message.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=\"%s\"; charset=\"%s\"\r\n\r\n%s\r\n\n", key, m.Charset, value)) } message.WriteString(fmt.Sprintf("--%s--", boundary)) } if c.secure == "SSL" || c.secure == "TLS" { return c.SendTLS(m, message) } return smtp.SendMail(c.host+":"+c.port, c.smtpAuth, m.From, m.To, message.Bytes()) } //SendTLS 通过TLS发送 func (c *SMTPClient) SendTLS(m Mail, message bytes.Buffer) error { var ct *smtp.Client var err error // TLS config tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: c.host, } // Here is the key, you need to call tls.Dial instead of smtp.Dial // for smtp servers running on 465 that require an ssl connection // from the very beginning (no starttls) conn, err := tls.Dial("tcp", c.host+":"+c.port, tlsconfig) if err != nil { log.Println(err, c.host) return err } ct, err = smtp.NewClient(conn, c.host) if err != nil { log.Println(err) return err } //if err := ct.StartTLS(tlsconfig);err != nil { // log.Println("StartTLS Error:",err,c.host,c.port) // return err //} //if err := ct.StartTLS(tlsconfig);err != nil { // fmt.Println(err) // return err //} fmt.Println(c.smtpAuth) if ok, s := ct.Extension("AUTH"); ok { logs.Info(s) // Auth if err = ct.Auth(c.smtpAuth); err != nil { log.Println("Auth Error:", err, c.user, ) return err } } // To && From if err = ct.Mail(m.From); err != nil { log.Println("Mail Error:", err, m.From) return err } for _, v := range m.To { if err := ct.Rcpt(v); err != nil { log.Println("Rcpt Error:", err, v) return err } } // Data w, err := ct.Data() if err != nil { log.Println("Data Object Error:", err) return err } _, err = w.Write(message.Bytes()) if err != nil { log.Println("Write Data Object Error:", err) return err } err = w.Close() if err != nil { log.Println("Data Object Close Error:", err) return err } ct.Quit() return nil } // AddTo will take a valid email address and store it in the mail. // It will return an error if the email is invalid. func (m *Mail) AddTo(email string) error { //Parses a single RFC 5322 address, e.g. "Barry Gibbs " parsedAddess, e := mail.ParseAddress(email) if e != nil { return e } m.AddRecipient(parsedAddess) return nil } // SetTos 设置收信人Email地址 func (m *Mail) SetTos(emails []string) { m.To = emails } // AddToName will add a new receipient name to mail func (m *Mail) AddToName(name string) { m.ToName = append(m.ToName, name) } // AddRecipient will take an already parsed mail.Address func (m *Mail) AddRecipient(receipient *mail.Address) { m.To = append(m.To, receipient.Address) if len(receipient.Name) > 0 { m.ToName = append(m.ToName, receipient.Name) } } // AddSubject will set the subject of the mail func (m *Mail) AddSubject(s string) { m.Subject = s } // AddHTML will set the body of the mail func (m *Mail) AddHTML(html string) { m.HTML = html } // AddText will set the body of the email func (m *Mail) AddText(text string) { m.Text = text } // AddFrom will set the senders email func (m *Mail) AddFrom(from string) error { //Parses a single RFC 5322 address, e.g. "Barry Gibbs " parsedAddess, e := mail.ParseAddress(from) if e != nil { return e } m.From = parsedAddess.Address m.FromName = parsedAddess.Name return nil } // AddBCC works like AddTo but for BCC func (m *Mail) AddBCC(email string) error { parsedAddess, e := mail.ParseAddress(email) if e != nil { return e } m.Bcc = append(m.Bcc, parsedAddess.Address) return nil } // AddRecipientBCC works like AddRecipient but for BCC func (m *Mail) AddRecipientBCC(email *mail.Address) { m.Bcc = append(m.Bcc, email.Address) } // AddFromName will set the senders name func (m *Mail) AddFromName(name string) { m.FromName = name } // AddReplyTo will set the return address func (m *Mail) AddReplyTo(reply string) { m.ReplyTo = reply } // AddDate specifies the date func (m *Mail) AddDate(date string) { m.Date = date } // AddAttachment will include file/s in mail func (m *Mail) AddAttachment(filePath string) error { if m.Files == nil { m.Files = make(map[string]string) } str, err := m.ReadAttachment(filePath) if err != nil { return err } _, filename := filepath.Split(filePath) m.Files[filename] = str return nil } // ReadAttachment reading attachment func (m *Mail) ReadAttachment(filePath string) (string, error) { file, e := ioutil.ReadFile(filePath) if e != nil { return "", e } encoded := base64.StdEncoding.EncodeToString(file) totalChars := len(encoded) maxLength := 500 //每行最大长度 totalLines := totalChars / maxLength var buf bytes.Buffer for i := 0; i < totalLines; i++ { buf.WriteString(encoded[i*maxLength:(i+1)*maxLength] + "\n") } buf.WriteString(encoded[totalLines*maxLength:]) return buf.String(), nil } // AddHeaders addding header string func (m *Mail) AddHeaders(headers string) { m.Headers = headers } // ======================================================= // unencryptedAuth // ======================================================= type unencryptedAuth struct { smtp.Auth } func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { s := *server s.TLS = true return a.Auth.Start(&s) } // ====================================================== // loginAuth // ====================================================== type loginAuth struct { username, password string } // LoginAuth loginAuth方式认证 func LoginAuth(username, password string) smtp.Auth { return &loginAuth{username, password} } func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { if !server.TLS { return "", nil, errors.New("unencrypted connection") } return "LOGIN", []byte(a.username), nil } func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { switch string(fromServer) { case "Username:": return []byte(a.username), nil case "Password:": return []byte(a.password), nil default: return nil, errors.New("Unkown fromServer") } } return nil, nil } ================================================ FILE: mail/smtp_test.go ================================================ package mail import ( // "os" "testing" ) func TestSend(t *testing.T) { /* conf := &SMTPConfig{ Username: "swh@adm***.com", Password: "", Host: "smtp.exmail.qq.com", Port: 465, Secure: "SSL", } c := NewSMTPClient(conf) m := NewMail() m.AddTo("brother <1556****@qq.com>") m.AddFrom("hank <" + conf.Username + ">") m.AddSubject("Testing") m.AddText("Some text :)") filepath, _ := os.Getwd() m.AddAttachment(filepath + "/README.md") if e := c.Send(m); e != nil { t.Error(e) } else { t.Log("发送成功") } */ } ================================================ FILE: mail/util.go ================================================ package mail import ( "net/mail" ) func MailAddr(name string, address string) *mail.Address { return &mail.Address{ Name: name, Address: address, } } type Attachments struct { Files []string BaseDir string } //SendMail 发送电邮 func SendMail(subject string, content string, receiver, sender string, bcc []string, smtpConfig *SMTPConfig, attachments *Attachments) error { c := NewSMTPClient(smtpConfig) m := NewMail() err := m.AddTo(receiver) //receiver e.g. "Barry Gibbs " if err != nil { return err } err = m.AddFrom(sender) if err != nil { return err } m.AddSubject(subject) //m.AddText("Some text :)") m.AddHTML(content) if attachments != nil { m.BaseDir = attachments.BaseDir for _, v := range attachments.Files { err = m.AddAttachment(v) if err != nil { return err } } } for _, addr := range bcc { err = m.AddBCC(addr) if err != nil { return err } } return c.Send(m) } ================================================ FILE: main.go ================================================ package main import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "runtime" "strings" _ "github.com/beego/beego/v2/server/web/session/memcache" _ "github.com/beego/beego/v2/server/web/session/mysql" _ "github.com/beego/beego/v2/server/web/session/redis" "github.com/kardianos/service" _ "github.com/mattn/go-sqlite3" "github.com/mindoc-org/mindoc/commands" "github.com/mindoc-org/mindoc/commands/daemon" _ "github.com/mindoc-org/mindoc/routers" ) func isViaDaemonUnix() bool { parentPid := os.Getppid() cmdLineBytes, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", parentPid)) if err != nil { return false } cmdLine := string(cmdLineBytes) executable := strings.Split(cmdLine, " ")[0] fmt.Printf("Parent executable: %s\n", executable) filename := filepath.Base(executable) return strings.Contains(filename, "mindoc-daemon") } func main() { if len(os.Args) >= 3 && os.Args[1] == "service" { if os.Args[2] == "install" { daemon.Install() } else if os.Args[2] == "remove" { daemon.Uninstall() } else if os.Args[2] == "restart" { daemon.Restart() } } commands.RegisterCommand() d := daemon.NewDaemon() if runtime.GOOS != "windows" && !isViaDaemonUnix() { s, err := service.New(d, d.Config()) if err != nil { fmt.Println("Create service error => ", err) os.Exit(1) } if err := s.Run(); err != nil { log.Fatal("启动程序失败 ->", err) } } else { d.Run() } } ================================================ FILE: mcp/handler.go ================================================ package mcp import ( "context" "encoding/json" "github.com/mark3labs/mcp-go/mcp" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/controllers" ) // GetGlobalSearchMcpTool 获取全局搜索的mcp工具 func GetGlobalSearchMcpTool() mcp.Tool { return mcp.NewTool("MinDocGlobalSearch", mcp.WithDescription("MinDoc全局文档内容搜索"), mcp.WithString("keyword", mcp.Required(), mcp.Description("要执行全局搜索的关键词,多个搜索关键词请用空格分割,请尽量使用最精简的关键词来检索,不要输入无关词汇"), ), mcp.WithNumber("pageIndex", mcp.Required(), mcp.Description("全局搜索时指定分页的顺序下标,每页最多有10条结果,建议只查看最相关的1-10页文档内容的搜索结果"), mcp.Enum("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"), ), ) } // GlobalSearchMcpHandler 全局搜索的mcp处理函数 func GlobalSearchMcpHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { paramMap := request.Params.Arguments.(map[string]any) pageIndex := 1 if v, ok := paramMap["pageIndex"].(float64); ok { pageIndex = int(v) } totalCount, result := globalSearchFunction(paramMap["keyword"].(string), pageIndex) jsonContent, err := json.Marshal(result) if err != nil { return mcp.NewToolResultStructuredOnly(map[string]any{ "totalCount": 0, "result": make([]map[string]any, 0), }), err } structContent := make([]map[string]any, 0) err = json.Unmarshal(jsonContent, &structContent) if err != nil { return mcp.NewToolResultStructuredOnly(map[string]any{ "totalCount": 0, "result": make([]map[string]any, 0), }), err } return mcp.NewToolResultStructuredOnly(map[string]any{ "totalCount": totalCount, "result": structContent, }), nil } func globalSearchFunction(keyword string, pageIndex int) (int, []*controllers.SearchV2RawResult) { memberId := 0 // 使用底层搜索函数 searchResult, _, totalCount, err := controllers.PerformSearchV2Raw(keyword, pageIndex, conf.PageSize, memberId) if err != nil { return 0, make([]*controllers.SearchV2RawResult, 0) } return totalCount, searchResult } ================================================ FILE: mcp/mcp.go ================================================ package mcp import ( "github.com/mark3labs/mcp-go/server" ) // MCPServer MinDoc MCP Server type MCPServer struct { server *server.MCPServer } // NewMCPServer creates a new MinDoc MCP Server func NewMCPServer() *MCPServer { mcpServer := server.NewMCPServer( "MinDoc MCP Server", "1.0.0", server.WithRecovery(), ) mcpServer.AddTool(GetGlobalSearchMcpTool(), GlobalSearchMcpHandler) return &MCPServer{ server: mcpServer, } } // ServeHTTP Run starts the server func (s *MCPServer) ServeHTTP() *server.StreamableHTTPServer { return server.NewStreamableHTTPServer(s.server) } ================================================ FILE: mcp/middleware.go ================================================ package mcp import ( "context" "net/http" "github.com/beego/beego/v2/server/web" beegoContext "github.com/beego/beego/v2/server/web/context" ) // AuthMiddleware 返回一个中间件函数,用于验证MCP请求中的认证令牌 func AuthMiddleware(ctx *beegoContext.Context) { presetMcpApiKey := web.AppConfig.DefaultString("mcp_api_key", "") mcpApiKeyParamValue := ctx.Request.URL.Query().Get("api_key") if presetMcpApiKey != mcpApiKeyParamValue { http.Error(ctx.ResponseWriter, "Missing or invalid mcp authorization key", http.StatusUnauthorized) return } // Add mcp_api_key to request context ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "mcp_api_key", mcpApiKeyParamValue)) } ================================================ FILE: models/AttachmentModel.go ================================================ // 数据库模型. package models import ( "time" "os" "strings" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils/filetil" // "gorm.io/driver/sqlite" // "gorm.io/gorm" // "gorm.io/gorm/logger" // "gorm.io/gorm/schema" ) // 定义全局的db对象,我们执行数据库操作主要通过他实现。 // var _db *gorm.DB // Attachment struct . type Attachment struct { AttachmentId int `orm:"column(attachment_id);pk;auto;unique" json:"attachment_id"` BookId int `orm:"column(book_id);type(int);description(所属book id)" json:"book_id"` DocumentId int `orm:"column(document_id);type(int);null;description(所属文档id)" json:"doc_id"` FileName string `orm:"column(file_name);size(255);description(文件名称)" json:"file_name"` FilePath string `orm:"column(file_path);size(2000);description(文件路径)" json:"file_path"` FileSize float64 `orm:"column(file_size);type(float);description(文件大小 字节)" json:"file_size"` HttpPath string `orm:"column(http_path);size(2000);description(文件路径)" json:"http_path"` FileExt string `orm:"column(file_ext);size(50);description(文件后缀)" json:"file_ext"` CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add;description(创建时间)" json:"create_time"` CreateAt int `orm:"column(create_at);type(int);description(创建人id)" json:"create_at"` ResourceType string `orm:"-" json:"resource_type"` } // TableName 获取对应上传附件数据库表名. func (m *Attachment) TableName() string { return "attachment" } // TableEngine 获取数据使用的引擎. func (m *Attachment) TableEngine() string { return "INNODB" } func (m *Attachment) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewAttachment() *Attachment { return &Attachment{} } func (m *Attachment) Insert() error { o := orm.NewOrm() _, err := o.Insert(m) return err } func (m *Attachment) Update() error { o := orm.NewOrm() _, err := o.Update(m) return err } func (m *Attachment) Delete() error { o := orm.NewOrm() _, err := o.Delete(m) if err == nil { if err1 := os.Remove(m.FilePath); err1 != nil { logs.Error(err1) } } return err } func (m *Attachment) Find(id int) (*Attachment, error) { if id <= 0 { return m, ErrInvalidParameter } o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("attachment_id", id).One(m) return m, err } // 查询指定文档的附件列表 func (m *Attachment) FindListByDocumentId(docId int) (attaches []*Attachment, err error) { o := orm.NewOrm() _, err = o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", docId).Filter("book_id__gt", 0).OrderBy("-attachment_id").All(&attaches) return } // 分页查询附件 func (m *Attachment) FindToPager(pageIndex, pageSize int) (attachList []*AttachmentResult, totalCount int, err error) { o := orm.NewOrm() total, err := o.QueryTable(m.TableNameWithPrefix()).Count() if err != nil { return nil, 0, err } totalCount = int(total) var list []*Attachment offset := (pageIndex - 1) * pageSize if pageSize == 0 { _, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-attachment_id").Offset(offset).Limit(pageSize).All(&list) } else { _, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-attachment_id").All(&list) } if err != nil { if err == orm.ErrNoRows { logs.Info("没有查到附件 ->", err) err = nil } return } for _, item := range list { attach := &AttachmentResult{} attach.Attachment = *item attach.FileShortSize = filetil.FormatBytes(int64(attach.FileSize)) //当项目ID为0标识是文章的附件 if item.BookId == 0 && item.DocumentId > 0 { blog := NewBlog() if err := o.QueryTable(blog.TableNameWithPrefix()).Filter("blog_id", item.DocumentId).One(blog, "blog_title"); err == nil { attach.BookName = blog.BlogTitle } else { attach.BookName = "[文章不存在]" } } else { book := NewBook() if e := o.QueryTable(book.TableNameWithPrefix()).Filter("book_id", item.BookId).One(book, "book_name"); e == nil { attach.BookName = book.BookName doc := NewDocument() if e := o.QueryTable(doc.TableNameWithPrefix()).Filter("document_id", item.DocumentId).One(doc, "document_name"); e == nil { attach.DocumentName = doc.DocumentName } else { attach.DocumentName = "[文档不存在]" } } else { attach.BookName = "[项目不存在]" } } attach.LocalHttpPath = strings.Replace(item.FilePath, "\\", "/", -1) attachList = append(attachList, attach) } return } ================================================ FILE: models/AttachmentResult.go ================================================ package models import ( "strings" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/utils/filetil" ) type AttachmentResult struct { Attachment IsExist bool BookName string DocumentName string FileShortSize string Account string LocalHttpPath string } func NewAttachmentResult() *AttachmentResult { return &AttachmentResult{IsExist: false} } func (m *AttachmentResult) Find(id int) (*AttachmentResult, error) { o := orm.NewOrm() attach := NewAttachment() err := o.QueryTable(m.TableNameWithPrefix()).Filter("attachment_id", id).One(attach) if err != nil { return m, err } m.Attachment = *attach if attach.BookId == 0 && attach.DocumentId > 0 { blog := NewBlog() if err := o.QueryTable(blog.TableNameWithPrefix()).Filter("blog_id", attach.DocumentId).One(blog, "blog_title"); err == nil { m.BookName = blog.BlogTitle } else { m.BookName = "[文章不存在]" } } else { book := NewBook() if e := o.QueryTable(book.TableNameWithPrefix()).Filter("book_id", attach.BookId).One(book, "book_name"); e == nil { m.BookName = book.BookName } else { m.BookName = "[不存在]" } doc := NewDocument() if e := o.QueryTable(doc.TableNameWithPrefix()).Filter("document_id", attach.DocumentId).One(doc, "document_name"); e == nil { m.DocumentName = doc.DocumentName } else { m.DocumentName = "[不存在]" } } if attach.CreateAt > 0 { member := NewMember() if e := o.QueryTable(member.TableNameWithPrefix()).Filter("member_id", attach.CreateAt).One(member, "account"); e == nil { m.Account = member.Account } } m.FileShortSize = filetil.FormatBytes(int64(attach.FileSize)) m.LocalHttpPath = strings.Replace(m.FilePath, "\\", "/", -1) return m, nil } ================================================ FILE: models/Auth2Account.go ================================================ // Package models . package models import ( "errors" "github.com/mindoc-org/mindoc/utils/auth2" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) var ( _ Auth2Account = (*WorkWeixinAccount)(nil) _ Auth2Account = (*DingTalkAccount)(nil) ) type Auth2Account interface { ExistedMember(id string) (*Member, error) AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error } func NewWorkWeixinAccount() *WorkWeixinAccount { return &WorkWeixinAccount{} } type WorkWeixinAccount struct { MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"` UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"` WorkWeixin_UserId string `orm:"size(100);unique;column(workweixin_user_id)" json:"workweixin_user_id"` // WorkWeixin_Name string `orm:"size(255);column(workweixin_name)" json:"workweixin_name"` // WorkWeixin_Phone string `orm:"size(25);column(workweixin_phone)" json:"workweixin_phone"` // WorkWeixin_Email string `orm:"size(255);column(workweixin_email)" json:"workweixin_email"` // WorkWeixin_Status int `orm:"type(int);column(status)" json:"status"` // WorkWeixin_Avatar string `orm:"size(1024);column(avatar)" json:"avatar"` CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` CreateAt int `orm:"type(int);column(create_at)" json:"create_at"` LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"` } // TableName 获取对应数据库表名. func (m *WorkWeixinAccount) TableName() string { return "workweixin_accounts" } // TableEngine 获取数据使用的引擎. func (m *WorkWeixinAccount) TableEngine() string { return "INNODB" } func (m *WorkWeixinAccount) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func (m *WorkWeixinAccount) ExistedMember(workweixin_user_id string) (*Member, error) { o := orm.NewOrm() account := NewWorkWeixinAccount() member := NewMember() err := o.QueryTable(m.TableNameWithPrefix()).Filter("workweixin_user_id", workweixin_user_id).One(account) if err != nil { return member, err } member, err = member.Find(account.MemberId) if err != nil { return member, err } if member.Status != 0 { return member, errors.New("receive_account_disabled") } return member, nil } // AddBind 添加一个用户. func (m *WorkWeixinAccount) AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error { tmpM := NewWorkWeixinAccount() err := o.QueryTable(m.TableNameWithPrefix()).Filter("workweixin_user_id", userInfo.UserId).One(tmpM) if err == nil { tmpM.MemberId = member.MemberId _, err = o.Update(tmpM) if err != nil { logs.Error("保存用户数据到数据时失败 =>", err) return errors.New("用户信息绑定失败, 数据库错误") } return nil } m.MemberId = member.MemberId m.WorkWeixin_UserId = userInfo.UserId if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", m.MemberId).Count(); err == nil && c > 0 { return errors.New("已绑定,不可重复绑定") } _, err = o.Insert(m) if err != nil { logs.Error("保存用户数据到数据时失败 =>", err) return errors.New("用户信息绑定失败, 数据库错误") } return nil } func NewDingTalkAccount() *DingTalkAccount { return &DingTalkAccount{} } type DingTalkAccount struct { MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"` UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"` Dingtalk_UserId string `orm:"size(100);unique;column(dingtalk_user_id)" json:"dingtalk_user_id"` CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` CreateAt int `orm:"type(int);column(create_at)" json:"create_at"` LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"` } // TableName 获取对应数据库表名. func (m *DingTalkAccount) TableName() string { return "dingtalk_accounts" } // TableEngine 获取数据使用的引擎. func (m *DingTalkAccount) TableEngine() string { return "INNODB" } func (m *DingTalkAccount) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func (m *DingTalkAccount) ExistedMember(userid string) (*Member, error) { o := orm.NewOrm() account := NewDingTalkAccount() member := NewMember() err := o.QueryTable(m.TableNameWithPrefix()).Filter("dingtalk_user_id", userid).One(account) if err != nil { return member, err } member, err = member.Find(account.MemberId) if err != nil { return member, err } if member.Status != 0 { return member, errors.New("receive_account_disabled") } return member, nil } // AddBind 添加一个用户. func (m *DingTalkAccount) AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error { tmpM := NewDingTalkAccount() err := o.QueryTable(m.TableNameWithPrefix()).Filter("dingtalk_user_id", userInfo.UserId).One(tmpM) if err == nil { tmpM.MemberId = member.MemberId _, err = o.Update(tmpM) if err != nil { logs.Error("保存用户数据到数据时失败 =>", err) return errors.New("用户信息绑定失败, 数据库错误") } return nil } m.Dingtalk_UserId = userInfo.UserId m.MemberId = member.MemberId if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", m.MemberId).Count(); err == nil && c > 0 { return errors.New("已绑定,不可重复绑定") } _, err = o.Insert(m) if err != nil { logs.Error("保存用户数据到数据时失败 =>", err) return errors.New("用户信息绑定失败, 数据库错误") } return nil } ================================================ FILE: models/Base.go ================================================ package models type Model struct { } ================================================ FILE: models/Blog.go ================================================ package models import ( "bytes" "fmt" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/mindoc-org/mindoc/cache" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" ) // 博文表 type Blog struct { BlogId int `orm:"pk;auto;unique;column(blog_id)" json:"blog_id"` //文章标题 BlogTitle string `orm:"column(blog_title);size(500);description(文章标题)" json:"blog_title"` //文章标识 BlogIdentify string `orm:"column(blog_identify);size(100);unique;description(文章标识)" json:"blog_identify"` //排序序号 OrderIndex int `orm:"column(order_index);type(int);default(0);description(排序序号)" json:"order_index"` //所属用户 MemberId int `orm:"column(member_id);type(int);default(0);index;description(所属用户)" json:"member_id"` //用户头像 MemberAvatar string `orm:"-" json:"member_avatar"` //文章类型:0 普通文章/1 链接文章 BlogType int `orm:"column(blog_type);type(int);default(0);description(文章类型: 0普通文章/1 链接文章)" json:"blog_type"` //链接到的项目中的文档ID DocumentId int `orm:"column(document_id);type(int);default(0);description(链接到的项目中的文档ID)" json:"document_id"` //文章的标识 DocumentIdentify string `orm:"-" json:"document_identify"` //关联文档的项目标识 BookIdentify string `orm:"-" json:"book_identify"` //关联文档的项目ID BookId int `orm:"-" json:"book_id"` //文章摘要 BlogExcerpt string `orm:"column(blog_excerpt);size(1500);description(文章摘要)" json:"blog_excerpt"` //文章内容 BlogContent string `orm:"column(blog_content);type(text);null;description(文章内容)" json:"blog_content"` //发布后的文章内容 BlogRelease string `orm:"column(blog_release);type(text);null;description(发布后的文章内容)" json:"blog_release"` //文章当前的状态,枚举enum(’publish’,’draft’,’password’)值,publish为已 发表,draft为草稿,password 为私人内容(不会被公开) 。默认为publish。 BlogStatus string `orm:"column(blog_status);size(100);default(publish);description(状态:publish为已发表-默认,draft:草稿,password :私人内容-不会被公开)" json:"blog_status"` //文章密码,varchar(100)值。文章编辑才可为文章设定一个密码,凭这个密码才能对文章进行重新强加或修改。 Password string `orm:"column(password);size(100);description(文章密码)" json:"-"` //最后修改时间 Modified time.Time `orm:"column(modify_time);type(datetime);auto_now;description(最后修改时间)" json:"modify_time"` //修改人id ModifyAt int `orm:"column(modify_at);type(int);description(修改人id)" json:"-"` ModifyRealName string `orm:"-" json:"modify_real_name"` //创建时间 Created time.Time `orm:"column(create_time);type(datetime);auto_now_add;description(创建时间)" json:"create_time"` CreateName string `orm:"-" json:"create_name"` //版本号 Version int64 `orm:"type(bigint);column(version);description(版本号)" json:"version"` //附件列表 AttachList []*Attachment `orm:"-" json:"attach_list"` } // 多字段唯一键 func (b *Blog) TableUnique() [][]string { return [][]string{ {"blog_id", "blog_identify"}, } } // TableName 获取对应数据库表名. func (b *Blog) TableName() string { return "blogs" } // TableEngine 获取数据使用的引擎. func (b *Blog) TableEngine() string { return "INNODB" } func (b *Blog) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + b.TableName() } func NewBlog() *Blog { return &Blog{ BlogStatus: "public", } } // 根据文章ID查询文章 func (b *Blog) Find(blogId int) (*Blog, error) { o := orm.NewOrm() err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_id", blogId).One(b) if err != nil { logs.Error("查询文章时失败 -> ", err) return nil, err } return b.Link() } // 从缓存中读取文章 func (b *Blog) FindFromCache(blogId int) (blog *Blog, err error) { key := fmt.Sprintf("blog-id-%d", blogId) var temp Blog err = cache.Get(key, &temp) if err == nil { b = &temp b.Link() logs.Debug("从缓存读取文章成功 ->", key) return b, nil } else { logs.Error("读取缓存失败 ->", err) } blog, err = b.Find(blogId) if err == nil { //默认一个小时 if err := cache.Put(key, blog, time.Hour*1); err != nil { logs.Error("将文章存入缓存失败 ->", err) } } return } // 查找指定用户的指定文章 func (b *Blog) FindByIdAndMemberId(blogId, memberId int) (*Blog, error) { o := orm.NewOrm() err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_id", blogId).Filter("member_id", memberId).One(b) if err != nil { logs.Error("查询文章时失败 -> ", err) return nil, err } return b.Link() } // 根据文章标识查询文章 func (b *Blog) FindByIdentify(identify string) (*Blog, error) { o := orm.NewOrm() err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_identify", identify).One(b) if err != nil { logs.Error("查询文章时失败 -> ", err) return nil, err } return b, nil } // 获取指定文章的链接内容 func (b *Blog) Link() (*Blog, error) { o := orm.NewOrm() //如果是链接文章,则需要从链接的项目中查找文章内容 if b.BlogType == 1 && b.DocumentId > 0 { doc := NewDocument() if err := o.QueryTable(doc.TableNameWithPrefix()).Filter("document_id", b.DocumentId).One(doc, "release", "markdown", "identify", "book_id"); err != nil { logs.Error("查询文章链接对象时出错 -> ", err) } else { b.DocumentIdentify = doc.Identify b.BlogRelease = doc.Release //目前仅支持markdown文档进行链接 b.BlogContent = doc.Markdown book := NewBook() if err := o.QueryTable(book.TableNameWithPrefix()).Filter("book_id", doc.BookId).One(book, "identify"); err != nil { logs.Error("查询关联文档的项目时出错 ->", err) } else { b.BookIdentify = book.Identify b.BookId = doc.BookId } //处理链接文档存在源文档修改时间的问题 if content, err := goquery.NewDocumentFromReader(bytes.NewBufferString(b.BlogRelease)); err == nil { content.Find(".wiki-bottom").Remove() if html, err := content.Html(); err == nil { b.BlogRelease = html } else { logs.Error("处理文章失败 ->", err) } } else { logs.Error("处理文章失败 ->", err) } } } if b.ModifyAt > 0 { member := NewMember() if err := o.QueryTable(member.TableNameWithPrefix()).Filter("member_id", b.ModifyAt).One(member, "real_name", "account"); err == nil { if member.RealName != "" { b.ModifyRealName = member.RealName } else { b.ModifyRealName = member.Account } } } if b.MemberId > 0 { member := NewMember() if err := o.QueryTable(member.TableNameWithPrefix()).Filter("member_id", b.MemberId).One(member, "real_name", "account", "avatar"); err == nil { if member.RealName != "" { b.CreateName = member.RealName } else { b.CreateName = member.Account } b.MemberAvatar = member.Avatar } } return b, nil } // 判断指定的文章标识是否存在 func (b *Blog) IsExist(identify string) bool { o := orm.NewOrm() return o.QueryTable(b.TableNameWithPrefix()).Filter("blog_identify", identify).Exist() } // 保存文章 func (b *Blog) Save(cols ...string) error { o := orm.NewOrm() if b.OrderIndex <= 0 { blog := NewBlog() if err := o.QueryTable(blog.TableNameWithPrefix()).OrderBy("-blog_id").Limit(1).One(blog, "blog_id"); err == nil { b.OrderIndex = blog.BlogId + 1 } else { c, _ := o.QueryTable(b.TableNameWithPrefix()).Count() b.OrderIndex = int(c) + 1 } } var err error b.Processor().Version = time.Now().Unix() if b.BlogId > 0 { b.Modified = time.Now() _, err = o.Update(b, cols...) key := fmt.Sprintf("blog-id-%d", b.BlogId) _ = cache.Delete(key) } else { b.Created = time.Now() _, err = o.Insert(b) } if err == nil && b.BlogId > 0 { // 刷新倒排索引 go func(blogId int, blogTitle, blogRelease, blogContent string) { content := blogRelease if content == "" { content = blogTitle + "\n" + blogContent } content = utils.StripTags(content) if err := BuildIndexForBlog(blogId, content); err != nil { logs.Error("构建Blog倒排索引失败 ->", blogId, err) } }(b.BlogId, b.BlogTitle, b.BlogRelease, b.BlogContent) } return err } // 过滤文章的危险标签,处理文章外链以及图片. func (b *Blog) Processor() *Blog { b.BlogRelease = utils.SafetyProcessor(b.BlogRelease) //解析文档中非本站的链接,并设置为新窗口打开 if content, err := goquery.NewDocumentFromReader(bytes.NewBufferString(b.BlogRelease)); err == nil { content.Find("a").Each(func(i int, contentSelection *goquery.Selection) { if src, ok := contentSelection.Attr("href"); ok { if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { //logs.Info(src,conf.BaseUrl,strings.HasPrefix(src,conf.BaseUrl)) if conf.BaseUrl != "" && !strings.HasPrefix(src, conf.BaseUrl) { contentSelection.SetAttr("target", "_blank") if html, err := content.Html(); err == nil { b.BlogRelease = html } } } } }) //设置图片为CDN地址 if cdnimg, _ := web.AppConfig.String("cdnimg"); cdnimg != "" { content.Find("img").Each(func(i int, contentSelection *goquery.Selection) { if src, ok := contentSelection.Attr("src"); ok && strings.HasPrefix(src, "/uploads/") { contentSelection.SetAttr("src", utils.JoinURI(cdnimg, src)) } }) } } return b } // 分页查询文章列表 func (b *Blog) FindToPager(pageIndex, pageSize int, memberId int, status string) (blogList []*Blog, totalCount int, err error) { o := orm.NewOrm() offset := (pageIndex - 1) * pageSize query := o.QueryTable(b.TableNameWithPrefix()) if memberId > 0 { query = query.Filter("member_id", memberId) } if status != "" && status != "all" { query = query.Filter("blog_status", status) } if status == "" { query = query.Filter("blog_status__ne", "private") } _, err = query.OrderBy("-order_index", "-blog_id").Offset(offset).Limit(pageSize).All(&blogList) if err != nil { if err == orm.ErrNoRows { err = nil } logs.Error("获取文章列表时出错 ->", err) return } count, err := query.Count() if err != nil { logs.Error("获取文章数量时出错 ->", err) return nil, 0, err } totalCount = int(count) for _, blog := range blogList { if blog.BlogType == 1 { blog.Link() } } return } // 删除文章 func (b *Blog) Delete(blogId int) error { // 删除文章缓存 key := fmt.Sprintf("blog-id-%d", blogId) _ = cache.Delete(key) o := orm.NewOrm() // 删除博客的倒排索引 index := NewContentReverseIndex() _ = index.DeleteByContentTypeAndContentId(2, blogId) _, err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_id", blogId).Delete() if err != nil { logs.Error("删除文章失败 ->", err) } return err } // 查询下一篇文章 func (b *Blog) QueryNext(blogId int) (*Blog, error) { o := orm.NewOrm() blog := NewBlog() if err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_id", blogId).One(blog, "order_index"); err != nil { logs.Error("查询文章时出错 ->", err) return b, err } err := o.QueryTable(b.TableNameWithPrefix()).Filter("order_index__gte", blog.OrderIndex).Filter("blog_id__gt", blogId).OrderBy("order_index", "blog_id").One(blog) if err != nil && err != orm.ErrNoRows { logs.Error("查询文章时出错 ->", err) } return blog, err } // 查询下一篇文章 func (b *Blog) QueryPrevious(blogId int) (*Blog, error) { o := orm.NewOrm() blog := NewBlog() if err := o.QueryTable(b.TableNameWithPrefix()).Filter("blog_id", blogId).One(blog, "order_index"); err != nil { logs.Error("查询文章时出错 ->", err) return b, err } err := o.QueryTable(b.TableNameWithPrefix()).Filter("order_index__lte", blog.OrderIndex).Filter("blog_id__lt", blogId).OrderBy("-order_index", "-blog_id").One(blog) if err != nil && err != orm.ErrNoRows { logs.Error("查询文章时出错 ->", err) } return blog, err } // 关联文章附件 func (b *Blog) LinkAttach() (err error) { o := orm.NewOrm() var attachList []*Attachment //当不是关联文章时,用文章ID去查询附件 if b.BlogType != 1 || b.DocumentId <= 0 { _, err = o.QueryTable(NewAttachment().TableNameWithPrefix()).Filter("document_id", b.BlogId).Filter("book_id", 0).All(&attachList) if err != nil && err != orm.ErrNoRows { logs.Error("查询文章附件时出错 ->", err) } } else { _, err = o.QueryTable(NewAttachment().TableNameWithPrefix()).Filter("document_id", b.DocumentId).Filter("book_id", b.BookId).All(&attachList) if err != nil && err != orm.ErrNoRows { logs.Error("查询文章附件时出错 ->", err) } } b.AttachList = attachList return } ================================================ FILE: models/BlogResult.go ================================================ package models type BlogResult struct{ } ================================================ FILE: models/BookModel.go ================================================ package models import ( "context" "crypto/md5" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "regexp" "strconv" "strings" "sync" "time" "encoding/json" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/cryptil" "github.com/mindoc-org/mindoc/utils/filetil" "github.com/mindoc-org/mindoc/utils/requests" "github.com/mindoc-org/mindoc/utils/ziptil" "github.com/russross/blackfriday/v2" ) var releaseQueue = make(chan int, 500) var once = sync.Once{} // Book struct . type Book struct { BookId int `orm:"pk;auto;unique;column(book_id)" json:"book_id"` // BookName 项目名称. BookName string `orm:"column(book_name);size(500);description(名称)" json:"book_name"` //所属项目空间 ItemId int `orm:"column(item_id);type(int);default(1);description(所属项目空间id)" json:"item_id"` // Identify 项目唯一标识. Identify string `orm:"column(identify);size(100);unique;description(唯一标识)" json:"identify"` //是否是自动发布 0 否/1 是 AutoRelease int `orm:"column(auto_release);type(int);default(0);description(是否是自动发布 0 否/1 是)" json:"auto_release"` //是否开启下载功能 0 是/1 否 IsDownload int `orm:"column(is_download);type(int);default(0);description(是否开启下载功能 0 是/1 否)" json:"is_download"` OrderIndex int `orm:"column(order_index);type(int);default(0);description(排序)" json:"order_index"` // Description 项目描述. Description string `orm:"column(description);size(2000);description(项目描述)" json:"description"` //发行公司 Publisher string `orm:"column(publisher);size(500);description(发行公司)" json:"publisher"` Label string `orm:"column(label);size(500);description(所属标签)" json:"label"` // PrivatelyOwned 项目私有: 0 公开/ 1 私有 PrivatelyOwned int `orm:"column(privately_owned);type(int);default(0);description(项目私有: 0 公开/ 1 私有)" json:"privately_owned"` // 当项目是私有时的访问Token. PrivateToken string `orm:"column(private_token);size(500);null;description(当项目是私有时的访问Token)" json:"private_token"` //访问密码. BookPassword string `orm:"column(book_password);size(500);null;description(访问密码)" json:"book_password"` //状态:0 正常/1 已删除 Status int `orm:"column(status);type(int);default(0);description(状态:0 正常/1 已删除)" json:"status"` //默认的编辑器. Editor string `orm:"column(editor);size(50);description(默认的编辑器 markdown/html)" json:"editor"` // DocCount 包含文档数量. DocCount int `orm:"column(doc_count);type(int);description(包含文档数量)" json:"doc_count"` // CommentStatus 评论设置的状态:open 为允许所有人评论,closed 为不允许评论, group_only 仅允许参与者评论 ,registered_only 仅允许注册者评论. CommentStatus string `orm:"column(comment_status);size(20);default(open);description(评论设置的状态:open 为允许所有人评论,closed 为不允许评论, group_only 仅允许参与者评论 ,registered_only 仅允许注册者评论.)" json:"comment_status"` CommentCount int `orm:"column(comment_count);type(int);description(评论数量)" json:"comment_count"` //封面地址 Cover string `orm:"column(cover);size(1000);description(封面地址)" json:"cover"` //主题风格 Theme string `orm:"column(theme);size(255);default(default);description(主题风格)" json:"theme"` // CreateTime 创建时间 . CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add;description(创建时间)" json:"create_time"` //每个文档保存的历史记录数量,0 为不限制 HistoryCount int `orm:"column(history_count);type(int);default(0);description(每个文档保存的历史记录数量,0 为不限制)" json:"history_count"` //是否启用分享,0启用/1不启用 IsEnableShare int `orm:"column(is_enable_share);type(int);default(0);description(是否启用分享,0启用/1不启用)" json:"is_enable_share"` MemberId int `orm:"column(member_id);size(100);description(作者id)" json:"member_id"` ModifyTime time.Time `orm:"type(datetime);column(modify_time);null;auto_now;description(修改时间)" json:"modify_time"` Version int64 `orm:"type(bigint);column(version);description(版本)" json:"version"` //是否使用第一篇文章项目为默认首页,0 否/1 是 IsUseFirstDocument int `orm:"column(is_use_first_document);type(int);default(0);description(是否使用第一篇文章项目为默认首页,0 否/1 是)" json:"is_use_first_document"` //是否开启自动保存:0 否/1 是 AutoSave int `orm:"column(auto_save);type(tinyint);default(0);description(是否开启自动保存:0 否/1 是)" json:"auto_save"` PrintSate int `orm:"column(print_state);type(tinyint);default(1);description(启用打印:0 否/1 是)" json:"print_state"` } func (book *Book) String() string { ret, err := json.Marshal(*book) if err != nil { return "" } return string(ret) } // TableName 获取对应数据库表名. func (book *Book) TableName() string { return "books" } // TableEngine 获取数据使用的引擎. func (book *Book) TableEngine() string { return "INNODB" } func (book *Book) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + book.TableName() } func (book *Book) QueryTable() orm.QuerySeter { return orm.NewOrm().QueryTable(book.TableNameWithPrefix()) } func NewBook() *Book { return &Book{} } // 添加一个项目 func (book *Book) Insert(lang string) error { o := orm.NewOrm() // o.Begin() book.BookName = utils.StripTags(book.BookName) if book.ItemId <= 0 { book.ItemId = 1 } _, err := o.Insert(book) if err == nil { if book.Label != "" { NewLabel().InsertOrUpdateMulti(book.Label) } relationship := NewRelationship() relationship.BookId = book.BookId relationship.RoleId = 0 relationship.MemberId = book.MemberId err = relationship.Insert() if err != nil { logs.Error("插入项目与用户关联 -> ", err) //o.Rollback() return err } document := NewDocument() document.BookId = book.BookId document.DocumentName = i18n.Tr(lang, "init.blank_doc") //"空白文档" document.MemberId = book.MemberId err = document.InsertOrUpdate() if err != nil { //o.Rollback() return err } //o.Commit() return nil } //o.Rollback() return err } func (book *Book) Find(id int, cols ...string) (*Book, error) { if id <= 0 { return book, ErrInvalidParameter } o := orm.NewOrm() err := o.QueryTable(book.TableNameWithPrefix()).Filter("book_id", id).One(book, cols...) return book, err } // 更新一个项目 func (book *Book) Update(cols ...string) error { o := orm.NewOrm() book.BookName = utils.StripTags(book.BookName) temp := NewBook() temp.BookId = book.BookId if err := o.Read(temp); err != nil { return err } if book.Label != "" || temp.Label != "" { go NewLabel().InsertOrUpdateMulti(book.Label + "," + temp.Label) } _, err := o.Update(book, cols...) return err } // 复制项目 func (book *Book) Copy(identify string) error { o := orm.NewOrm() err := o.QueryTable(book.TableNameWithPrefix()).Filter("identify", identify).One(book) if err != nil { logs.Error("查询项目时出错 -> ", err) return err } if _, err := o.Begin(); err != nil { logs.Error("开启事物时出错 -> ", err) return err } bookId := book.BookId book.BookId = 0 book.Identify = book.Identify + fmt.Sprintf("%s-%s", identify, strconv.FormatInt(time.Now().UnixNano(), 32)) book.BookName = book.BookName + "[副本]" book.CreateTime = time.Now() book.CommentCount = 0 book.HistoryCount = 0 /* v2 version of beego remove the o.Rollback api for transaction operation. * typically, in v1, you can write code like this: * * o := orm.NewOrm() * if err := o.Operateion(); err != nil { * o.Rollback() * ... * } * * however, in v2, this is not available. beego will handles the transaction in new way using * cluster. the new code is like below: * * o := orm.NewOrm() * if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error{ * err := o.Operations() * if err != nil { * return err * } * ... * }); err != nil { * ... * } * * when operation failed, it will automatically calls o.Rollback() for TxOrmer. * more details see https://beego.me/docs/mvc/model/transaction.md */ if err := o.DoTx(func(ctx context.Context, txo orm.TxOrmer) error { _, err := txo.Insert(book) return err }); err != nil { logs.Error("复制项目时出错: %s", err) return err } var rels []*Relationship if err := o.DoTx(func(ctx context.Context, txo orm.TxOrmer) error { _, err := txo.QueryTable(NewRelationship().TableNameWithPrefix()).Filter("book_id", bookId).All(&rels) return err }); err != nil { logs.Error("复制项目关系时出错 -> ", err) return err } for _, rel := range rels { rel.BookId = book.BookId rel.RelationshipId = 0 if err := o.DoTx(func(ctx context.Context, txo orm.TxOrmer) error { _, err := txo.Insert(rel) return err }); err != nil { logs.Error("复制项目关系时出错 -> ", err) return err } } var docs []*Document if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err := txOrm.QueryTable(NewDocument().TableNameWithPrefix()).Filter("book_id", bookId).Filter("parent_id", 0).All(&docs) return err }); err != nil && err != orm.ErrNoRows { logs.Error("读取项目文档时出错 -> ", err) return err } if len(docs) > 0 { if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { return recursiveInsertDocument(docs, txOrm, book.BookId, 0) }); err != nil { logs.Error("复制项目时出错 -> ", err) return err } } return nil } // 递归的复制文档 func recursiveInsertDocument(docs []*Document, o orm.TxOrmer, bookId int, parentId int) error { for _, doc := range docs { docId := doc.DocumentId doc.DocumentId = 0 doc.ParentId = parentId doc.BookId = bookId doc.Version = time.Now().Unix() if _, err := o.Insert(doc); err != nil { logs.Error("插入项目时出错 -> ", err) return err } var attachList []*Attachment //读取所有附件列表 if _, err := o.QueryTable(NewAttachment().TableNameWithPrefix()).Filter("document_id", docId).All(&attachList); err == nil { for _, attach := range attachList { attach.BookId = bookId attach.DocumentId = doc.DocumentId attach.AttachmentId = 0 if _, err := o.Insert(attach); err != nil { return err } } } var subDocs []*Document if _, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("parent_id", docId).All(&subDocs); err != nil && err != orm.ErrNoRows { logs.Error("读取文档时出错 -> ", err) return err } if len(subDocs) > 0 { if err := recursiveInsertDocument(subDocs, o, bookId, doc.DocumentId); err != nil { return err } } } return nil } // 根据指定字段查询结果集. func (book *Book) FindByField(field string, value interface{}, cols ...string) ([]*Book, error) { o := orm.NewOrm() var books []*Book _, err := o.QueryTable(book.TableNameWithPrefix()).Filter(field, value).All(&books, cols...) return books, err } // 根据指定字段查询一个结果. func (book *Book) FindByFieldFirst(field string, value interface{}) (*Book, error) { o := orm.NewOrm() err := o.QueryTable(book.TableNameWithPrefix()).Filter(field, value).One(book) return book, err } // 根据项目标识查询项目 func (book *Book) FindByIdentify(identify string, cols ...string) (*Book, error) { o := orm.NewOrm() err := o.QueryTable(book.TableNameWithPrefix()).Filter("identify", identify).One(book, cols...) return book, err } // 分页查询指定用户的项目 func (book *Book) FindToPager(pageIndex, pageSize, memberId int, lang string) (books []*BookResult, totalCount int, err error) { o := orm.NewOrm() //sql1 := "SELECT COUNT(book.book_id) AS total_count FROM " + book.TableNameWithPrefix() + " AS book LEFT JOIN " + // relationship.TableNameWithPrefix() + " AS rel ON book.book_id=rel.book_id AND rel.member_id = ? WHERE rel.relationship_id > 0 " sql1 := `SELECT count(*) AS total_count FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.member_id = ? left join (select book_id,min(role_id) as role_id from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by t.book_id) as team on team.book_id=book.book_id WHERE rel.role_id >= 0 or team.role_id >= 0` err = o.Raw(sql1, memberId, memberId).QueryRow(&totalCount) if err != nil { return } offset := (pageIndex - 1) * pageSize //sql2 := "SELECT book.*,rel.member_id,rel.role_id,m.account as create_name FROM " + book.TableNameWithPrefix() + " AS book" + // " LEFT JOIN " + relationship.TableNameWithPrefix() + " AS rel ON book.book_id=rel.book_id AND rel.member_id = ?" + // " LEFT JOIN " + relationship.TableNameWithPrefix() + " AS rel1 ON book.book_id=rel1.book_id AND rel1.role_id=0" + // " LEFT JOIN " + NewMember().TableNameWithPrefix() + " AS m ON rel1.member_id=m.member_id " + // " WHERE rel.relationship_id > 0 ORDER BY book.order_index DESC,book.book_id DESC LIMIT " + fmt.Sprintf("%d,%d", pageSize, offset) sql2 := `SELECT book.*, case when rel.relationship_id is null then team.role_id else rel.role_id end as role_id, m.account as create_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.member_id = ? left join (select book_id,min(role_id) as role_id from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by book_id) as team on team.book_id=book.book_id LEFT JOIN md_relationship AS rel1 ON book.book_id = rel1.book_id AND rel1.role_id = 0 LEFT JOIN md_members AS m ON rel1.member_id = m.member_id WHERE rel.role_id >= 0 or team.role_id >= 0 ORDER BY book.order_index, book.book_id DESC limit ? offset ?` _, err = o.Raw(sql2, memberId, memberId, pageSize, offset).QueryRows(&books) if err != nil { logs.Error("分页查询项目列表 => ", err) return } sql := "SELECT m.account,doc.modify_time FROM md_documents AS doc LEFT JOIN md_members AS m ON doc.modify_at=m.member_id WHERE book_id = ? LIMIT 1 ORDER BY doc.modify_time DESC" if len(books) > 0 { for index, book := range books { var text struct { Account string ModifyTime time.Time } err1 := o.Raw(sql, book.BookId).QueryRow(&text) if err1 == nil { books[index].LastModifyText = text.Account + " 于 " + text.ModifyTime.Format("2006-01-02 15:04:05") } if book.RoleId == 0 { book.RoleName = i18n.Tr(lang, "common.creator") } else if book.RoleId == 1 { book.RoleName = i18n.Tr(lang, "common.administrator") } else if book.RoleId == 2 { book.RoleName = i18n.Tr(lang, "common.editor") } else if book.RoleId == 3 { book.RoleName = i18n.Tr(lang, "common.observer") } } } return } // 彻底删除项目. func (book *Book) ThoroughDeleteBook(id int) error { if id <= 0 { return ErrInvalidParameter } o := orm.NewOrm() book, err := book.Find(id) if err != nil { return err } o.Begin() //删除附件,这里没有删除实际物理文件 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw("DELETE FROM "+NewAttachment().TableNameWithPrefix()+" WHERE book_id=?", book.BookId).Exec() return err }); err != nil { return err } //删除该项目下所有文档的倒排索引 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { // 先查询该项目下的所有文档ID,然后删除对应的倒排索引 var docIds []int _, err := txOrm.Raw("SELECT document_id FROM "+NewDocument().TableNameWithPrefix()+" WHERE book_id = ?", book.BookId).QueryRows(&docIds) if err == nil && len(docIds) > 0 { indexTable := NewContentReverseIndex().TableNameWithPrefix() // 删除 content_type=1 (Document) 且 content_id 在 docIds 中的倒排索引 placeholders := make([]string, len(docIds)) args := make([]interface{}, len(docIds)+1) args[0] = 1 // content_type=1 for i, id := range docIds { placeholders[i] = "?" args[i+1] = id } sql := "DELETE FROM " + indexTable + " WHERE content_type = ? AND content_id IN (" + strings.Join(placeholders, ",") + ")" _, _ = txOrm.Raw(sql, args...).Exec() } return nil }); err != nil { // 倒排索引删除失败不影响主流程,记日志即可 logs.Error("删除项目文档倒排索引失败 ->", book.BookId, err) } //删除文档 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw("DELETE FROM "+NewDocument().TableNameWithPrefix()+" WHERE book_id = ?", book.BookId).Exec() return err }); err != nil { return err } //删除项目 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw("DELETE FROM "+book.TableNameWithPrefix()+" WHERE book_id = ?", book.BookId).Exec() return err }); err != nil { return err } //删除关系 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw("DELETE FROM "+NewRelationship().TableNameWithPrefix()+" WHERE book_id = ?", book.BookId).Exec() return err }); err != nil { return err } if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw(fmt.Sprintf("DELETE FROM %s WHERE book_id=?", NewTeamRelationship().TableNameWithPrefix()), book.BookId).Exec() return err }); err != nil { return err } //删除模板 if err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { _, err = txOrm.Raw("DELETE FROM "+NewTemplate().TableNameWithPrefix()+" WHERE book_id = ?", book.BookId).Exec() return err }); err != nil { return err } if book.Label != "" { NewLabel().InsertOrUpdateMulti(book.Label) } //删除导出缓存 if err := os.RemoveAll(filepath.Join(conf.GetExportOutputPath(), strconv.Itoa(id))); err != nil { logs.Error("删除项目缓存失败 ->", err) } //删除附件和图片 if err := os.RemoveAll(filepath.Join(conf.WorkingDirectory, "uploads", book.Identify)); err != nil { logs.Error("删除项目附件和图片失败 ->", err) } return nil } // 分页查找系统首页数据. func (book *Book) FindForHomeToPager(pageIndex, pageSize, memberId int) (books []*BookResult, totalCount int, err error) { o := orm.NewOrm() offset := (pageIndex - 1) * pageSize //如果是登录用户 if memberId > 0 { sql1 := `SELECT COUNT(*) FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select book_id,min(role_id) AS role_id from (select book_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by book_id) as team on team.book_id=book.book_id WHERE book.privately_owned = 0 or rel.role_id >=0 or team.role_id >=0` err = o.Raw(sql1, memberId, memberId).QueryRow(&totalCount) if err != nil { return } sql2 := `SELECT book.*,rel1.*,mdmb.account AS create_name,mdmb.real_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select book_id,min(role_id) AS role_id from (select book_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by book_id) as team on team.book_id=book.book_id LEFT JOIN md_relationship AS rel1 ON rel1.book_id = book.book_id AND rel1.role_id = 0 LEFT JOIN md_members AS mdmb ON rel1.member_id = mdmb.member_id WHERE book.privately_owned = 0 or rel.role_id >=0 or team.role_id >=0 ORDER BY order_index desc,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql2, memberId, memberId, pageSize, offset).QueryRows(&books) } else { count, err1 := o.QueryTable(book.TableNameWithPrefix()).Filter("privately_owned", 0).Count() if err1 != nil { err = err1 return } totalCount = int(count) sql := `SELECT book.*,rel.*,mdmb.account AS create_name,mdmb.real_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id WHERE book.privately_owned = 0 ORDER BY order_index DESC ,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql, pageSize, offset).QueryRows(&books) } return } // 分页全局搜索. func (book *Book) FindForLabelToPager(keyword string, pageIndex, pageSize, memberId int) (books []*BookResult, totalCount int, err error) { o := orm.NewOrm() keyword = "%" + keyword + "%" offset := (pageIndex - 1) * pageSize //如果是登录用户 if memberId > 0 { sql1 := `SELECT COUNT(*) FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select * from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.role_id,t.team_member_id,t.book_id) as team on team.book_id = book.book_id WHERE (relationship_id > 0 OR book.privately_owned = 0 or team.team_member_id > 0) AND book.label LIKE ?` err = o.Raw(sql1, memberId, memberId, keyword).QueryRow(&totalCount) if err != nil { return } sql2 := `SELECT book.*,rel1.*,mdmb.account AS create_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select * from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.role_id,t.team_member_id,t.book_id) as team on team.book_id = book.book_id LEFT JOIN md_relationship AS rel1 ON rel1.book_id = book.book_id AND rel1.role_id = 0 LEFT JOIN md_members AS mdmb ON rel1.member_id = mdmb.member_id WHERE (rel.relationship_id > 0 OR book.privately_owned = 0 or team.team_member_id > 0) AND book.label LIKE ? ORDER BY order_index DESC ,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql2, memberId, memberId, keyword, pageSize, offset).QueryRows(&books) return } else { count, err1 := o.QueryTable(NewBook().TableNameWithPrefix()).Filter("privately_owned", 0).Filter("label__icontains", keyword).Count() if err1 != nil { err = err1 return } totalCount = int(count) sql := `SELECT book.*,rel.*,mdmb.account AS create_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id WHERE book.privately_owned = 0 AND book.label LIKE ? ORDER BY order_index DESC ,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql, keyword, pageSize, offset).QueryRows(&books) return } } // ReleaseContent 批量发布文档 func (book *Book) ReleaseContent(bookId int, lang string) { releaseQueue <- bookId once.Do(func() { go func() { defer func() { if err := recover(); err != nil { logs.Error("协程崩溃 ->", err) } }() for bookId := range releaseQueue { o := orm.NewOrm() var docs []*Document _, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("book_id", bookId).All(&docs) if err != nil { logs.Error("发布失败 =>", bookId, err) continue } for _, item := range docs { item.BookId = bookId item.Lang = lang _ = item.ReleaseContent() } //当文档发布后,需要删除已缓存的转换项目 outputPath := filepath.Join(conf.GetExportOutputPath(), strconv.Itoa(bookId)) _ = os.RemoveAll(outputPath) } }() }) } // 重置文档数量 func (book *Book) ResetDocumentNumber(bookId int) { o := orm.NewOrm() totalCount, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("book_id", bookId).Count() if err == nil { _, err = o.Raw("UPDATE md_books SET doc_count = ? WHERE book_id = ?", int(totalCount), bookId).Exec() if err != nil { logs.Error("重置文档数量失败 =>", bookId, err) } } else { logs.Error("获取文档数量失败 =>", bookId, err) } } // 导入zip项目 func (book *Book) ImportBook(zipPath string, lang string) error { if !filetil.FileExists(zipPath) { return errors.New("文件不存在 => " + zipPath) } w := md5.New() io.WriteString(w, zipPath) //将str写入到w中 io.WriteString(w, time.Now().String()) io.WriteString(w, book.BookName) md5str := fmt.Sprintf("%x", w.Sum(nil)) //w.Sum(nil)将w的hash转成[]byte格式 tempPath := filepath.Join(os.TempDir(), md5str) if err := os.MkdirAll(tempPath, 0766); err != nil { logs.Error("创建导入目录出错 => ", err) } //如果加压缩失败 if err := ziptil.Unzip(zipPath, tempPath); err != nil { logs.Error("CAll ziptil.Unzip error, zipPath: %s, tempPath: %s, err: %v", zipPath, tempPath, err) return err } //当导入结束后,删除临时文件 //defer os.RemoveAll(tempPath) for { //如果当前目录下只有一个目录,则重置根目录 if entries, err := ioutil.ReadDir(tempPath); err == nil && len(entries) == 1 { dir := entries[0] if dir.IsDir() && dir.Name() != "." && dir.Name() != ".." { tempPath = filepath.Join(tempPath, dir.Name()) break } } else { break } } tempPath = strings.Replace(tempPath, "\\", "/", -1) docMap := make(map[string]int, 0) o := orm.NewOrm() o.Insert(book) relationship := NewRelationship() relationship.BookId = book.BookId relationship.RoleId = 0 relationship.MemberId = book.MemberId relationship.Insert() err := filepath.Walk(tempPath, func(path string, info os.FileInfo, err error) error { path = strings.Replace(path, "\\", "/", -1) if path == tempPath { return nil } if !info.IsDir() { ext := filepath.Ext(info.Name()) //如果是Markdown文件 if strings.EqualFold(ext, ".md") || strings.EqualFold(ext, ".markdown") { logs.Info("正在处理 =>", path, info.Name()) doc := NewDocument() doc.BookId = book.BookId doc.MemberId = book.MemberId docIdentify := strings.Replace(strings.TrimPrefix(path, tempPath+"/"), "/", "-", -1) if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, docIdentify); !ok || err != nil { docIdentify = "import-" + docIdentify } doc.Identify = docIdentify //匹配图片,如果图片语法是在代码块中,这里同样会处理 re := regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`) markdown, err := filetil.ReadFileAndIgnoreUTF8BOM(path) if err != nil { return err } //处理图片 doc.Markdown = re.ReplaceAllStringFunc(string(markdown), func(image string) string { images := re.FindAllSubmatch([]byte(image), -1) if len(images) <= 0 || len(images[0]) < 3 { return image } originalImageUrl := string(images[0][2]) imageUrl := strings.Replace(string(originalImageUrl), "\\", "/", -1) //如果是本地路径,则需要将图片复制到项目目录 if !strings.HasPrefix(imageUrl, "http://") && !strings.HasPrefix(imageUrl, "https://") && !strings.HasPrefix(imageUrl, "ftp://") { //如果路径中存在参数 if l := strings.Index(imageUrl, "?"); l > 0 { imageUrl = imageUrl[:l] } if strings.HasPrefix(imageUrl, "/") { imageUrl = filepath.Join(tempPath, imageUrl) } else if strings.HasPrefix(imageUrl, "./") { imageUrl = filepath.Join(filepath.Dir(path), strings.TrimPrefix(imageUrl, "./")) } else if strings.HasPrefix(imageUrl, "../") { imageUrl = filepath.Join(filepath.Dir(path), imageUrl) } else { imageUrl = filepath.Join(filepath.Dir(path), imageUrl) } imageUrl = strings.Replace(imageUrl, "\\", "/", -1) dstFile := filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), strings.TrimPrefix(imageUrl, tempPath)) if filetil.FileExists(imageUrl) { filetil.CopyFile(imageUrl, dstFile) imageUrl = strings.TrimPrefix(strings.Replace(dstFile, "\\", "/", -1), strings.Replace(conf.WorkingDirectory, "\\", "/", -1)) if !strings.HasPrefix(imageUrl, "/") && !strings.HasPrefix(imageUrl, "\\") { imageUrl = "/" + imageUrl } } } else { imageExt := cryptil.Md5Crypt(imageUrl) + filepath.Ext(imageUrl) dstFile := filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), imageExt) if err := requests.DownloadAndSaveFile(imageUrl, dstFile); err == nil { imageUrl = strings.TrimPrefix(strings.Replace(dstFile, "\\", "/", -1), strings.Replace(conf.WorkingDirectory, "\\", "/", -1)) if !strings.HasPrefix(imageUrl, "/") && !strings.HasPrefix(imageUrl, "\\") { imageUrl = "/" + imageUrl } } } imageUrl = strings.Replace(strings.TrimSuffix(image, originalImageUrl+")")+conf.URLForWithCdnImage(imageUrl)+")", "\\", "/", -1) return imageUrl }) linkRegexp := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) //处理链接 doc.Markdown = linkRegexp.ReplaceAllStringFunc(doc.Markdown, func(link string) string { links := linkRegexp.FindAllStringSubmatch(link, -1) originalLink := links[0][2] var linkPath string var err error if strings.HasPrefix(originalLink, "<") { originalLink = strings.TrimPrefix(originalLink, "<") } if strings.HasSuffix(originalLink, ">") { originalLink = strings.TrimSuffix(originalLink, ">") } //如果是从根目录开始, if strings.HasPrefix(originalLink, "/") { linkPath, err = filepath.Abs(filepath.Join(tempPath, originalLink)) } else if strings.HasPrefix(originalLink, "./") { linkPath, err = filepath.Abs(filepath.Join(filepath.Dir(path), originalLink[1:])) } else { linkPath, err = filepath.Abs(filepath.Join(filepath.Dir(path), originalLink)) } if err == nil { //如果本地存在该链接 if filetil.FileExists(linkPath) { ext := filepath.Ext(linkPath) //logs.Info("当前后缀 -> ",ext) //如果链接是Markdown文件,则生成文档标识,否则,将目标文件复制到项目目录 if strings.EqualFold(ext, ".md") || strings.EqualFold(ext, ".markdown") { docIdentify := strings.Replace(strings.TrimPrefix(strings.Replace(linkPath, "\\", "/", -1), tempPath+"/"), "/", "-", -1) //logs.Info(originalLink, "|", linkPath, "|", docIdentify) if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, docIdentify); !ok || err != nil { docIdentify = "import-" + docIdentify } docIdentify = strings.TrimSuffix(docIdentify, "-README.md") link = strings.TrimSuffix(link, originalLink+")") + conf.URLFor("DocumentController.Read", ":key", book.Identify, ":id", docIdentify) + ")" } else { dstPath := filepath.Join(conf.WorkingDirectory, "uploads", time.Now().Format("200601"), originalLink) filetil.CopyFile(linkPath, dstPath) tempLink := conf.BaseUrl + strings.TrimPrefix(strings.Replace(dstPath, "\\", "/", -1), strings.Replace(conf.WorkingDirectory, "\\", "/", -1)) link = strings.TrimSuffix(link, originalLink+")") + tempLink + ")" } } else { logs.Info("文件不存在 ->", linkPath) } } return link }) //codeRe := regexp.MustCompile("```\\w+") //doc.Markdown = codeRe.ReplaceAllStringFunc(doc.Markdown, func(s string) string { // //logs.Info(s) // return strings.Replace(s,"```","``` ",-1) //}) doc.Content = string(blackfriday.Run([]byte(doc.Markdown))) doc.Version = time.Now().Unix() //解析文档名称,默认使用第一个h标签为标题 docName := strings.TrimSuffix(info.Name(), ext) for _, line := range strings.Split(doc.Markdown, "\n") { if strings.HasPrefix(line, "#") { docName = strings.TrimLeft(line, "#") break } } doc.DocumentName = strings.TrimSpace(docName) parentId := 0 parentIdentify := strings.Replace(strings.Trim(strings.TrimSuffix(strings.TrimPrefix(path, tempPath), info.Name()), "/"), "/", "-", -1) if parentIdentify != "" { if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, parentIdentify); !ok || err != nil { parentIdentify = "import-" + parentIdentify } if id, ok := docMap[parentIdentify]; ok { parentId = id } } if strings.EqualFold(info.Name(), "README.md") { logs.Info(path, "|", info.Name(), "|", parentIdentify, "|", parentId) } isInsert := false //如果当前文件是README.md,则将内容更新到父级 if strings.EqualFold(info.Name(), "README.md") && parentId != 0 { doc.DocumentId = parentId //logs.Info(path,"|",parentId) } else { //logs.Info(path,"|",parentIdentify) doc.ParentId = parentId isInsert = true } if err := doc.InsertOrUpdate("document_name", "markdown", "content"); err != nil { logs.Error(doc.DocumentId, err) } if isInsert { docMap[docIdentify] = doc.DocumentId } } } else { //如果当前目录下存在Markdown文件,则需要创建此节点 if filetil.HasFileOfExt(path, []string{".md", ".markdown"}) { logs.Info("正在处理 =>", path, info.Name()) identify := strings.Replace(strings.Trim(strings.TrimPrefix(path, tempPath), "/"), "/", "-", -1) if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, identify); !ok || err != nil { identify = "import-" + identify } parentDoc := NewDocument() parentDoc.MemberId = book.MemberId parentDoc.BookId = book.BookId parentDoc.Identify = identify parentDoc.Version = time.Now().Unix() parentDoc.DocumentName = "空白文档" parentId := 0 parentIdentify := strings.TrimSuffix(identify, "-"+info.Name()) if id, ok := docMap[parentIdentify]; ok { parentId = id } parentDoc.ParentId = parentId if err := parentDoc.InsertOrUpdate(); err != nil { logs.Error(err) } docMap[identify] = parentDoc.DocumentId //logs.Info(path,"|",parentDoc.DocumentId,"|",identify,"|",info.Name(),"|",parentIdentify) } } return nil }) if err != nil { logs.Error("导入项目异常 => ", err) book.Description = "【项目导入存在错误:" + err.Error() + "】" } logs.Info("项目导入完毕 => ", book.BookName) book.ReleaseContent(book.BookId, lang) return err } // 导入docx项目 func (book *Book) ImportWordBook(docxPath string, lang string) (err error) { if !filetil.FileExists(docxPath) { return errors.New("文件不存在") } docxPath = strings.Replace(docxPath, "\\", "/", -1) o := orm.NewOrm() o.Insert(book) relationship := NewRelationship() relationship.BookId = book.BookId relationship.RoleId = 0 relationship.MemberId = book.MemberId err = relationship.Insert() if err != nil { logs.Error("插入项目与用户关联 -> ", err) return err } doc := NewDocument() doc.BookId = book.BookId doc.MemberId = book.MemberId docIdentify := strings.Replace(strings.TrimPrefix(docxPath, os.TempDir()+"/"), "/", "-", -1) if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, docIdentify); !ok || err != nil { docIdentify = "import-" + docIdentify } doc.Identify = docIdentify if doc.Markdown, err = utils.Docx2md(docxPath, false); err != nil { logs.Error("导入doc项目转换异常 => ", err) return err } // fmt.Println("===doc.Markdown===") // fmt.Println(doc.Markdown) // fmt.Println("==================") doc.Content = string(blackfriday.Run([]byte(doc.Markdown))) // fmt.Println("===doc.Content===") // fmt.Println(doc.Content) // fmt.Println("==================") doc.Version = time.Now().Unix() var docName string for _, line := range strings.Split(doc.Markdown, "\n") { if strings.HasPrefix(line, "#") { docName = strings.TrimLeft(line, "#") break } } doc.DocumentName = strings.TrimSpace(docName) if err := doc.InsertOrUpdate("document_name", "book_id", "markdown", "content"); err != nil { logs.Error(doc.DocumentId, err) } if err != nil { logs.Error("导入项目异常 => ", err) book.Description = "【项目导入存在错误:" + err.Error() + "】" } logs.Info("项目导入完毕 => ", book.BookName) book.ReleaseContent(book.BookId, lang) return err } func (book *Book) FindForRoleId(bookId, memberId int) (conf.BookRole, error) { o := orm.NewOrm() var relationship Relationship err := NewRelationship().QueryTable().Filter("book_id", bookId).Filter("member_id", memberId).One(&relationship) if err != nil && err != orm.ErrNoRows { return 0, err } if err == nil { return relationship.RoleId, nil } sql := `select role_id from md_team_relationship as mtr left join md_team_member as mtm using (team_id) where mtr.book_id = ? and mtm.member_id = ? order by mtm.role_id asc limit 1;` var roleId int err = o.Raw(sql, bookId, memberId).QueryRow(&roleId) if err != nil { logs.Error("查询用户项目角色出错 -> book_id=", bookId, " member_id=", memberId, err) return 0, err } return conf.BookRole(roleId), nil } ================================================ FILE: models/BookResult.go ================================================ package models import ( "bytes" "io/ioutil" "os" "path/filepath" "strconv" "strings" "time" "encoding/json" "net/http" "regexp" "github.com/PuerkitoBio/goquery" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/converter" "github.com/mindoc-org/mindoc/utils/cryptil" "github.com/mindoc-org/mindoc/utils/filetil" "github.com/mindoc-org/mindoc/utils/gopool" "github.com/mindoc-org/mindoc/utils/requests" "github.com/mindoc-org/mindoc/utils/ziptil" "github.com/russross/blackfriday/v2" ) var ( exportLimitWorkerChannel = gopool.NewChannelPool(conf.GetExportLimitNum(), conf.GetExportQueueLimitNum()) ) type BookResult struct { BookId int `json:"book_id"` BookName string `json:"book_name"` ItemId int `json:"item_id"` ItemName string `json:"item_name"` Identify string `json:"identify"` OrderIndex int `json:"order_index"` Description string `json:"description"` Publisher string `json:"publisher"` PrivatelyOwned int `json:"privately_owned"` PrivateToken string `json:"private_token"` BookPassword string `json:"book_password"` DocCount int `json:"doc_count"` CommentStatus string `json:"comment_status"` CommentCount int `json:"comment_count"` CreateTime time.Time `json:"create_time"` CreateName string `json:"create_name"` RealName string `json:"real_name"` ModifyTime time.Time `json:"modify_time"` Cover string `json:"cover"` Theme string `json:"theme"` Label string `json:"label"` MemberId int `json:"member_id"` Editor string `json:"editor"` AutoRelease bool `json:"auto_release"` HistoryCount int `json:"history_count"` //RelationshipId int `json:"relationship_id"` //TeamRelationshipId int `json:"team_relationship_id"` RoleId conf.BookRole `json:"role_id"` RoleName string `json:"role_name"` Status int `json:"status"` IsEnableShare bool `json:"is_enable_share"` IsUseFirstDocument bool `json:"is_use_first_document"` LastModifyText string `json:"last_modify_text"` IsDisplayComment bool `json:"is_display_comment"` IsDownload bool `json:"is_download"` AutoSave bool `json:"auto_save"` PrintState bool `json:"print_state"` Lang string } func NewBookResult() *BookResult { return &BookResult{} } func (m *BookResult) String() string { ret, err := json.Marshal(*m) if err != nil { return "" } return string(ret) } func (m *BookResult) SetLang(lang string) *BookResult { m.Lang = lang return m } // 根据项目标识查询项目以及指定用户权限的信息. func (m *BookResult) FindByIdentify(identify string, memberId int) (*BookResult, error) { if identify == "" || memberId <= 0 { return m, ErrInvalidParameter } o := orm.NewOrm() var book Book err := NewBook().QueryTable().Filter("identify", identify).One(&book) if err != nil { logs.Error("获取项目失败 ->", err) return m, err } roleId, err := NewBook().FindForRoleId(book.BookId, memberId) if err != nil { return m, ErrPermissionDenied } var relationship2 Relationship //查找项目创始人 err = NewRelationship().QueryTable().Filter("book_id", book.BookId).Filter("role_id", 0).One(&relationship2) if err != nil { logs.Error("根据项目标识查询项目以及指定用户权限的信息 -> ", err) return m, ErrPermissionDenied } member, err := NewMember().Find(relationship2.MemberId) if err != nil { return m, err } m.ToBookResult(book) m.RoleId = roleId m.MemberId = memberId m.CreateName = member.Account if member.RealName != "" { m.RealName = member.RealName } if m.RoleId == conf.BookFounder { m.RoleName = i18n.Tr(m.Lang, "common.creator") } else if m.RoleId == conf.BookAdmin { m.RoleName = i18n.Tr(m.Lang, "common.administrator") } else if m.RoleId == conf.BookEditor { m.RoleName = i18n.Tr(m.Lang, "common.editor") } else if m.RoleId == conf.BookObserver { m.RoleName = i18n.Tr(m.Lang, "common.observer") } doc := NewDocument() err = o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", book.BookId).OrderBy("modify_time").One(doc) if err == nil { member2 := NewMember() member2.Find(doc.ModifyAt) m.LastModifyText = member2.Account + " 于 " + doc.ModifyTime.Local().Format("2006-01-02 15:04:05") } return m, nil } func (m *BookResult) FindToPager(pageIndex, pageSize int) (books []*BookResult, totalCount int, err error) { o := orm.NewOrm() count, err := o.QueryTable(NewBook().TableNameWithPrefix()).Count() if err != nil { return } totalCount = int(count) sql := `SELECT book.*,rel.relationship_id,rel.role_id,m.account AS create_name,m.real_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0 LEFT JOIN md_members AS m ON rel.member_id = m.member_id ORDER BY book.order_index DESC ,book.book_id DESC limit ? offset ?` offset := (pageIndex - 1) * pageSize _, err = o.Raw(sql, pageSize, offset).QueryRows(&books) return } // 实体转换 func (m *BookResult) ToBookResult(book Book) *BookResult { m.BookId = book.BookId m.BookName = book.BookName m.Identify = book.Identify m.OrderIndex = book.OrderIndex m.Description = strings.Replace(book.Description, "\r\n", "
", -1) m.PrivatelyOwned = book.PrivatelyOwned m.PrivateToken = book.PrivateToken m.BookPassword = book.BookPassword m.DocCount = book.DocCount m.CommentStatus = book.CommentStatus m.CommentCount = book.CommentCount m.CreateTime = book.CreateTime m.ModifyTime = book.ModifyTime m.Cover = book.Cover m.Label = book.Label m.Status = book.Status m.Editor = book.Editor m.Theme = book.Theme m.AutoRelease = book.AutoRelease == 1 m.IsEnableShare = book.IsEnableShare == 0 m.IsUseFirstDocument = book.IsUseFirstDocument == 1 m.Publisher = book.Publisher m.HistoryCount = book.HistoryCount m.IsDownload = book.IsDownload == 0 m.AutoSave = book.AutoSave == 1 m.PrintState = book.PrintSate == 1 m.ItemId = book.ItemId m.RoleId = conf.BookRoleNoSpecific if book.Theme == "" { m.Theme = "default" } if book.Editor == "" { m.Editor = "markdown" } doc := NewDocument() o := orm.NewOrm() err := o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", book.BookId).OrderBy("modify_time").One(doc) if err == nil { member2 := NewMember() member2.Find(doc.ModifyAt) m.LastModifyText = member2.Account + " 于 " + doc.ModifyTime.Local().Format("2006-01-02 15:04:05") } if m.ItemId > 0 { if item, err := NewItemsets().First(m.ItemId); err == nil { m.ItemName = item.ItemName } } if m.CommentStatus == "closed" { m.IsDisplayComment = false } else if m.CommentStatus == "open" { m.IsDisplayComment = true } else if m.CommentStatus == "registered_only" { // todo } else if m.CommentStatus == "group_only" { // todo } else { m.IsDisplayComment = false } return m } // 后台转换 func BackgroundConvert(sessionId string, bookResult *BookResult) error { if err := converter.CheckConvertCommand(); err != nil { logs.Error("检查转换程序失败 -> ", err) return err } err := exportLimitWorkerChannel.LoadOrStore(bookResult.Identify, func() { bookResult.Converter(sessionId) }) if err != nil { logs.Error("将导出任务加入任务队列失败 -> ", err) return err } exportLimitWorkerChannel.Start() return nil } // 导出PDF、word等格式 func (m *BookResult) Converter(sessionId string) (ConvertBookResult, error) { convertBookResult := ConvertBookResult{} outputPath := filepath.Join(conf.GetExportOutputPath(), strconv.Itoa(m.BookId)) viewPath := web.BConfig.WebConfig.ViewsPath pdfpath := filepath.Join(outputPath, "book.pdf") epubpath := filepath.Join(outputPath, "book.epub") mobipath := filepath.Join(outputPath, "book.mobi") docxpath := filepath.Join(outputPath, "book.docx") //先将转换的文件储存到临时目录 tempOutputPath := filepath.Join(os.TempDir(), sessionId, m.Identify, "source") //filepath.Abs(filepath.Join("cache", sessionId)) sourceDir := strings.TrimSuffix(tempOutputPath, "source") if filetil.FileExists(sourceDir) { if err := os.RemoveAll(sourceDir); err != nil { logs.Error("删除临时目录失败 ->", sourceDir, err) } } if err := os.MkdirAll(outputPath, 0766); err != nil { logs.Error("创建目录失败 -> ", outputPath, err) } if err := os.MkdirAll(tempOutputPath, 0766); err != nil { logs.Error("创建目录失败 -> ", tempOutputPath, err) } os.MkdirAll(filepath.Join(tempOutputPath, "Images"), 0755) //defer os.RemoveAll(strings.TrimSuffix(tempOutputPath,"source")) if filetil.FileExists(pdfpath) && filetil.FileExists(epubpath) && filetil.FileExists(mobipath) && filetil.FileExists(docxpath) { convertBookResult.EpubPath = epubpath convertBookResult.MobiPath = mobipath convertBookResult.PDFPath = pdfpath convertBookResult.WordPath = docxpath return convertBookResult, nil } docs, err := NewDocument().FindListByBookId(m.BookId) if err != nil { return convertBookResult, err } tocList := make([]converter.Toc, 0) for _, item := range docs { if item.ParentId == 0 { toc := converter.Toc{ Id: item.DocumentId, Link: strconv.Itoa(item.DocumentId) + ".html", Pid: item.ParentId, Title: item.DocumentName, } tocList = append(tocList, toc) } } for _, item := range docs { if item.ParentId != 0 { toc := converter.Toc{ Id: item.DocumentId, Link: strconv.Itoa(item.DocumentId) + ".html", Pid: item.ParentId, Title: item.DocumentName, } tocList = append(tocList, toc) } } ebookConfig := converter.Config{ Charset: "utf-8", Cover: m.Cover, Timestamp: time.Now().Format("2006-01-02 15:04:05"), Description: string(blackfriday.Run([]byte(m.Description))), Footer: "

本文档使用 MinDoc 构建 - _PAGENUM_ -

", Header: "

_SECTION_

", Identifier: "", Language: "zh-CN", Creator: m.CreateName, Publisher: m.Publisher, Contributor: m.Publisher, Title: m.BookName, Format: []string{"epub", "mobi", "pdf", "docx"}, FontSize: "14", PaperSize: "a4", MarginLeft: "72", MarginRight: "72", MarginTop: "72", MarginBottom: "72", Toc: tocList, More: []string{}, } if m.Publisher != "" { ebookConfig.Footer = "

本文档由 " + m.Publisher + " 生成- _PAGENUM_ -

" } else if web.AppConfig.DefaultString("publisher_def", "") != "" { defPub := web.AppConfig.DefaultString("publisher_def", "") ebookConfig.Footer = "

本文档由 " + defPub + " 生成- _PAGENUM_ -

" } if m.RealName != "" { ebookConfig.Creator = m.RealName } if tempOutputPath, err = filepath.Abs(tempOutputPath); err != nil { logs.Error("导出目录配置错误:" + err.Error()) return convertBookResult, err } for _, item := range docs { name := strconv.Itoa(item.DocumentId) fpath := filepath.Join(tempOutputPath, name+".html") f, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { return convertBookResult, err } var buf bytes.Buffer if err := web.ExecuteViewPathTemplate(&buf, "document/export.tpl", viewPath, map[string]interface{}{"Model": m, "Lists": item, "BaseUrl": conf.BaseUrl}); err != nil { return convertBookResult, err } html := buf.String() if err != nil { f.Close() return convertBookResult, err } bufio := bytes.NewReader(buf.Bytes()) doc, err := goquery.NewDocumentFromReader(bufio) doc.Find("img").Each(func(i int, contentSelection *goquery.Selection) { if src, ok := contentSelection.Attr("src"); ok { //var encodeString string dstSrcString := "Images/" + filepath.Base(src) //如果是本地路径则直接读取文件内容 if strings.HasPrefix(src, "/") { spath := filepath.Join(conf.WorkingDirectory, src) if filetil.CopyFile(spath, filepath.Join(tempOutputPath, dstSrcString)); err != nil { logs.Error("复制图片失败 -> ", err, src) return } } else { client := &http.Client{} if req, err := http.NewRequest("GET", src, nil); err == nil { req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36") req.Header.Set("Referer", src) //10秒连接超时时间 client.Timeout = time.Second * 100 if resp, err := client.Do(req); err == nil { defer resp.Body.Close() if body, err := ioutil.ReadAll(resp.Body); err == nil { //encodeString = base64.StdEncoding.EncodeToString(body) if err := ioutil.WriteFile(filepath.Join(tempOutputPath, dstSrcString), body, 0755); err != nil { logs.Error("下载图片失败 -> ", err, src) return } } else { logs.Error("下载图片失败 -> ", err, src) return } } else { logs.Error("下载图片失败 -> ", err, src) return } } } contentSelection.SetAttr("src", dstSrcString) } }) //移除文档底部的更新信息 if selection := doc.Find("div.wiki-bottom").First(); selection.Size() > 0 { selection.Remove() } html, err = doc.Html() if err != nil { f.Close() return convertBookResult, err } f.WriteString(html) f.Close() } if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "kancloud.css"), filepath.Join(tempOutputPath, "styles", "css", "kancloud.css")); err != nil { logs.Error("复制CSS样式出错 -> static/css/kancloud.css", err) } if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "export.css"), filepath.Join(tempOutputPath, "styles", "css", "export.css")); err != nil { logs.Error("复制CSS样式出错 -> static/css/export.css", err) } if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "editor.md", "css", "editormd.preview.css"), filepath.Join(tempOutputPath, "styles", "editor.md", "css", "editormd.preview.css")); err != nil { logs.Error("复制CSS样式出错 -> static/editor.md/css/editormd.preview.css", err) } if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "markdown.preview.css"), filepath.Join(tempOutputPath, "styles", "css", "markdown.preview.css")); err != nil { logs.Error("复制CSS样式出错 -> static/css/markdown.preview.css", err) } if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "editor.md", "lib", "highlight", "styles", "github.css"), filepath.Join(tempOutputPath, "styles", "css", "github.css")); err != nil { logs.Error("复制CSS样式出错 -> static/editor.md/lib/highlight/styles/github.css", err) } if err := filetil.CopyDir(filepath.Join(conf.WorkingDirectory, "static", "font-awesome"), filepath.Join(tempOutputPath, "styles", "font-awesome")); err != nil { logs.Error("复制CSS样式出错 -> static/font-awesome", err) } eBookConverter := &converter.Converter{ BasePath: tempOutputPath, OutputPath: filepath.Join(strings.TrimSuffix(tempOutputPath, "source"), "output"), Config: ebookConfig, Debug: true, ProcessNum: conf.GetExportProcessNum(), } os.MkdirAll(eBookConverter.OutputPath, 0766) if err := eBookConverter.Convert(); err != nil { logs.Error("转换文件错误:" + m.BookName + " -> " + err.Error()) return convertBookResult, err } logs.Info("文档转换完成:" + m.BookName) if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath, "output", "book.mobi"), mobipath); err != nil { logs.Error("复制文档失败 -> ", filepath.Join(eBookConverter.OutputPath, "output", "book.mobi"), err) } if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath, "output", "book.pdf"), pdfpath); err != nil { logs.Error("复制文档失败 -> ", filepath.Join(eBookConverter.OutputPath, "output", "book.pdf"), err) } if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath, "output", "book.epub"), epubpath); err != nil { logs.Error("复制文档失败 -> ", filepath.Join(eBookConverter.OutputPath, "output", "book.epub"), err) } if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath, "output", "book.docx"), docxpath); err != nil { logs.Error("复制文档失败 -> ", filepath.Join(eBookConverter.OutputPath, "output", "book.docx"), err) } convertBookResult.MobiPath = mobipath convertBookResult.PDFPath = pdfpath convertBookResult.EpubPath = epubpath convertBookResult.WordPath = docxpath return convertBookResult, nil } // 导出Markdown原始文件 func (m *BookResult) ExportMarkdown(sessionId string) (string, error) { outputPath := filepath.Join(conf.WorkingDirectory, "uploads", "books", strconv.Itoa(m.BookId), "book.zip") os.MkdirAll(filepath.Dir(outputPath), 0644) tempOutputPath := filepath.Join(os.TempDir(), sessionId, "markdown") defer os.RemoveAll(tempOutputPath) bookUrl := conf.URLFor("DocumentController.Index", ":key", m.Identify) + "/" err := exportMarkdown(tempOutputPath, 0, m.BookId, tempOutputPath, bookUrl) if err != nil { return "", err } if err := ziptil.Compress(outputPath, tempOutputPath); err != nil { logs.Error("导出Markdown失败->", err) return "", err } return outputPath, nil } // 递归导出Markdown文档 func exportMarkdown(p string, parentId int, bookId int, baseDir string, bookUrl string) error { o := orm.NewOrm() var docs []*Document _, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("book_id", bookId).Filter("parent_id", parentId).All(&docs) if err != nil { logs.Error("导出Markdown失败->", err) return err } for _, doc := range docs { //获取当前文档的子文档数量,如果数量不为0,则将当前文档命名为READMD.md并设置成目录。 subDocCount, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("parent_id", doc.DocumentId).Count() if err != nil { logs.Error("导出Markdown失败->", err) return err } var docPath string if subDocCount > 0 { if doc.Identify != "" { docPath = filepath.Join(p, doc.Identify, "README.md") } else { docPath = filepath.Join(p, strconv.Itoa(doc.DocumentId), "README.md") } } else { if doc.Identify != "" { if strings.HasSuffix(doc.Identify, ".md") || strings.HasSuffix(doc.Identify, ".markdown") { docPath = filepath.Join(p, doc.Identify) } else { docPath = filepath.Join(p, doc.Identify+".md") } } else { docPath = filepath.Join(p, strings.TrimSpace(doc.DocumentName)+".md") } } dirPath := filepath.Dir(docPath) os.MkdirAll(dirPath, 0766) markdown := doc.Markdown //如果当前文档不为空 if strings.TrimSpace(doc.Markdown) != "" { re := regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`) //处理文档中图片 markdown = re.ReplaceAllStringFunc(doc.Markdown, func(image string) string { images := re.FindAllSubmatch([]byte(image), -1) if len(images) <= 0 || len(images[0]) < 3 { return image } originalImageUrl := string(images[0][2]) imageUrl := strings.Replace(string(originalImageUrl), "\\", "/", -1) //如果是本地路径,则需要将图片复制到项目目录 if strings.HasPrefix(imageUrl, "http://") || strings.HasPrefix(imageUrl, "https://") { imageExt := cryptil.Md5Crypt(imageUrl) + filepath.Ext(imageUrl) dstFile := filepath.Join(baseDir, "uploads", time.Now().Format("200601"), imageExt) if err := requests.DownloadAndSaveFile(imageUrl, dstFile); err == nil { imageUrl = strings.TrimPrefix(strings.Replace(dstFile, "\\", "/", -1), strings.Replace(baseDir, "\\", "/", -1)) if !strings.HasPrefix(imageUrl, "/") && !strings.HasPrefix(imageUrl, "\\") { imageUrl = "/" + imageUrl } } } else if strings.HasPrefix(imageUrl, "/") { filetil.CopyFile(filepath.Join(conf.WorkingDirectory, imageUrl), filepath.Join(baseDir, imageUrl)) } imageUrl = strings.Replace(strings.TrimSuffix(image, originalImageUrl+")")+imageUrl+")", "\\", "/", -1) return imageUrl }) linkRe := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) markdown = linkRe.ReplaceAllStringFunc(markdown, func(link string) string { links := linkRe.FindAllStringSubmatch(link, -1) if len(links) > 0 && len(links[0]) >= 3 { originalLink := links[0][2] //如果当前链接位于当前项目内 if strings.HasPrefix(originalLink, bookUrl) { docIdentify := strings.TrimSpace(strings.TrimPrefix(originalLink, bookUrl)) tempDoc := NewDocument() if id, err := strconv.Atoi(docIdentify); err == nil && id > 0 { err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("document_id", id).One(tempDoc, "identify", "parent_id", "document_id") if err != nil { logs.Error(err) return link } } else { err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("identify", docIdentify).One(tempDoc, "identify", "parent_id", "document_id") if err != nil { logs.Error(err) return link } } tempLink := recursiveJoinDocumentIdentify(tempDoc.ParentId, "") + strings.TrimPrefix(originalLink, bookUrl) if !strings.HasSuffix(tempLink, ".md") && !strings.HasSuffix(doc.Identify, ".markdown") { tempLink = tempLink + ".md" } relative := strings.TrimPrefix(strings.Replace(p, "\\", "/", -1), strings.Replace(baseDir, "\\", "/", -1)) repeat := 0 if relative != "" { relative = strings.TrimSuffix(strings.TrimPrefix(relative, "/"), "/") repeat = strings.Count(relative, "/") + 1 } logs.Info(repeat, "|", relative, "|", p, "|", baseDir) tempLink = strings.Repeat("../", repeat) + tempLink link = strings.TrimSuffix(link, originalLink+")") + tempLink + ")" } } return link }) } else { markdown = "# " + doc.DocumentName + "\n" } if err := ioutil.WriteFile(docPath, []byte(markdown), 0644); err != nil { logs.Error("导出Markdown失败->", err) return err } if subDocCount > 0 { if err = exportMarkdown(dirPath, doc.DocumentId, bookId, baseDir, bookUrl); err != nil { return err } } } return nil } func recursiveJoinDocumentIdentify(parentDocId int, identify string) string { o := orm.NewOrm() doc := NewDocument() err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("document_id", parentDocId).One(doc, "identify", "parent_id", "document_id") if err != nil { logs.Error(err) return identify } if doc.Identify == "" { identify = strconv.Itoa(doc.DocumentId) + "/" + identify } else { identify = doc.Identify + "/" + identify } if doc.ParentId > 0 { identify = recursiveJoinDocumentIdentify(doc.ParentId, identify) } return identify } // 查询项目的第一篇文档 func (m *BookResult) FindFirstDocumentByBookId(bookId int) (*Document, error) { o := orm.NewOrm() doc := NewDocument() err := o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", bookId).Filter("parent_id", 0).OrderBy("order_sort").One(doc) return doc, err } ================================================ FILE: models/CommentModel.go ================================================ package models import ( "errors" "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) // Comment struct type Comment struct { CommentId int `orm:"pk;auto;unique;column(comment_id)" json:"comment_id"` Floor int `orm:"column(floor);type(unsigned);default(0)" json:"floor"` BookId int `orm:"column(book_id);type(int)" json:"book_id"` // DocumentId 评论所属的文档. DocumentId int `orm:"column(document_id);type(int)" json:"document_id"` // Author 评论作者. Author string `orm:"column(author);size(100)" json:"author"` //MemberId 评论用户ID. MemberId int `orm:"column(member_id);type(int)" json:"member_id"` // IPAddress 评论者的IP地址 IPAddress string `orm:"column(ip_address);size(100)" json:"ip_address"` // 评论日期. CommentDate time.Time `orm:"type(datetime);column(comment_date);auto_now_add" json:"comment_date"` //Content 评论内容. Content string `orm:"column(content);size(2000)" json:"content"` // Approved 评论状态:0 待审核/1 已审核/2 垃圾评论/ 3 已删除 Approved int `orm:"column(approved);type(int)" json:"approved"` // UserAgent 评论者浏览器内容 UserAgent string `orm:"column(user_agent);size(500)" json:"user_agent"` // Parent 评论所属父级 ParentId int `orm:"column(parent_id);type(int);default(0)" json:"parent_id"` AgreeCount int `orm:"column(agree_count);type(int);default(0)" json:"agree_count"` AgainstCount int `orm:"column(against_count);type(int);default(0)" json:"against_count"` Index int `orm:"-" json:"index"` ShowDel int `orm:"-" json:"show_del"` Avatar string `orm:"-" json:"avatar"` } // TableName 获取对应数据库表名. func (m *Comment) TableName() string { return "comments" } // TableEngine 获取数据使用的引擎. func (m *Comment) TableEngine() string { return "INNODB" } func (m *Comment) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewComment() *Comment { return &Comment{} } // 是否有权限删除 func (m *Comment) CanDelete(user_memberid int, user_bookrole conf.BookRole) bool { return user_memberid == m.MemberId || user_bookrole == conf.BookFounder || user_bookrole == conf.BookAdmin } // 根据文档id查询文档评论 func (m *Comment) QueryCommentByDocumentId(doc_id, page, pagesize int, member *Member) (comments []*Comment, count int64, ret_page int) { doc, err := NewDocument().Find(doc_id) if err != nil { return } o := orm.NewOrm() count, _ = o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", doc_id).Count() if -1 == page { // 请求最后一页 var total int = int(count) if total%pagesize == 0 { page = total / pagesize } else { page = total/pagesize + 1 } } offset := (page - 1) * pagesize ret_page = page o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", doc_id).OrderBy("comment_date").Offset(offset).Limit(pagesize).All(&comments) // 需要判断未登录的情况 var bookRole conf.BookRole if member != nil { bookRole, _ = NewRelationship().FindForRoleId(doc.BookId, member.MemberId) } for i := 0; i < len(comments); i++ { comments[i].Index = (i + 1) + (page-1)*pagesize if member != nil && comments[i].CanDelete(member.MemberId, bookRole) { comments[i].ShowDel = 1 comments[i].Avatar = member.Avatar } } return } func (m *Comment) Update(cols ...string) error { o := orm.NewOrm() _, err := o.Update(m, cols...) return err } // Insert 添加一条评论. func (m *Comment) Insert() error { if m.DocumentId <= 0 { return errors.New("评论文档不存在") } if m.Content == "" { return ErrCommentContentNotEmpty } o := orm.NewOrm() if m.CommentId > 0 { comment := NewComment() //如果父评论不存在 if err := o.Read(comment); err != nil { return err } } document := NewDocument() //如果评论的文档不存在 if _, err := document.Find(m.DocumentId); err != nil { return err } book, err := NewBook().Find(document.BookId) //如果评论的项目不存在 if err != nil { return err } //如果已关闭评论 if book.CommentStatus == "closed" { return ErrCommentClosed } if book.CommentStatus == "registered_only" && m.MemberId <= 0 { return ErrPermissionDenied } //如果仅参与者评论 if book.CommentStatus == "group_only" { if m.MemberId <= 0 { return ErrPermissionDenied } rel := NewRelationship() if _, err := rel.FindForRoleId(book.BookId, m.MemberId); err != nil { return ErrPermissionDenied } } if m.MemberId > 0 { member := NewMember() //如果用户不存在 if _, err := member.Find(m.MemberId); err != nil { return ErrMemberNoExist } //如果用户被禁用 if member.Status == 1 { return ErrMemberDisabled } } else if m.Author == "" { m.Author = "[匿名用户]" } m.BookId = book.BookId _, err = o.Insert(m) return err } // 删除一条评论 func (m *Comment) Delete() error { o := orm.NewOrm() _, err := o.Delete(m) return err } func (m *Comment) Find(id int, cols ...string) (*Comment, error) { o := orm.NewOrm() if err := o.QueryTable(m.TableNameWithPrefix()).Filter("comment_id", id).One(m, cols...); err != nil { return m, err } return m, nil } ================================================ FILE: models/ContentReverseIndex.go ================================================ package models import ( "crypto/md5" "encoding/hex" "errors" "fmt" "math" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/segmenter" ) func init() { //go InitializeMissingIndexes() } // ContentReverseIndex 倒排索引结构 type ContentReverseIndex struct { Id string `orm:"pk;column(id);size(64);description(唯一标识ID)" json:"id"` // Word 分词词汇,最长64个字 Word string `orm:"column(word);size(64);index;description(分词词汇)" json:"word"` // ContentType 内容类型:1-Document 2-Blog ContentType int `orm:"column(content_type);type(int);index:idx_content_type_id,priority:1;description(内容类型:1-Document 2-Blog)" json:"content_type"` // ContentId 内容ID,对应DocumentId或BlogId ContentId int `orm:"column(content_id);type(int);index:idx_content_type_id,priority:2;description(内容ID)" json:"content_id"` // WordCount 词频数 WordCount int `orm:"column(word_count);type(int);default(0);description(词频数)" json:"word_count"` } // TableName 获取对应数据库表名 func (c *ContentReverseIndex) TableName() string { return "t_content_reverse_index" } // TableEngine 获取数据使用的引擎 func (c *ContentReverseIndex) TableEngine() string { return "INNODB" } func (c *ContentReverseIndex) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + c.TableName() } func NewContentReverseIndex() *ContentReverseIndex { return &ContentReverseIndex{} } // Insert 插入倒排索引记录 func (c *ContentReverseIndex) Insert() error { if c.Id == "" { return errors.New("id不能为空") } if c.Word == "" { return errors.New("分词词汇不能为空") } if c.ContentType != 1 && c.ContentType != 2 { return errors.New("内容类型必须是1(Document)或2(Blog)") } if c.ContentId <= 0 { return errors.New("内容ID必须大于0") } o := orm.NewOrm() _, err := o.Insert(c) return err } // DeleteByContentTypeAndContentId 根据内容类型和内容ID删除所有倒排索引记录 func (c *ContentReverseIndex) DeleteByContentTypeAndContentId(contentType, contentId int) error { if contentType != 1 && contentType != 2 { return errors.New("内容类型必须是1(Document)或2(Blog)") } if contentId <= 0 { return errors.New("内容ID必须大于0") } o := orm.NewOrm() _, err := o.QueryTable(c.TableNameWithPrefix()).Filter("content_type", contentType).Filter("content_id", contentId).Delete() return err } // BatchInsert 批量插入倒排索引记录 func (c *ContentReverseIndex) BatchInsert(indices []*ContentReverseIndex) error { if len(indices) == 0 { return nil } o := orm.NewOrm() _, err := o.InsertMulti(len(indices), indices) return err } // ContentReverseIndexResult 倒排索引查询结果结构 type ContentReverseIndexResult struct { ContentId int `json:"content_id"` ContentType int `json:"content_type"` Score float64 `json:"score"` // TF-IDF分数 WordCounts []int `json:"word_counts"` // 各个词的词频 } // FindByWordsWithPagination 根据多个分词词汇分页批量查询结果,按IDF值排序 // words: 分词词汇列表 // pageIndex: 页码,从1开始 // pageSize: 每页数量 func (c *ContentReverseIndex) FindByWordsWithPagination(words []string, pageIndex, pageSize int) ([]*ContentReverseIndexResult, int, error) { if len(words) == 0 { return nil, 0, errors.New("分词词汇列表不能为空") } if pageIndex <= 0 { pageIndex = 1 } if pageSize <= 0 { pageSize = 10 } o := orm.NewOrm() tableName := c.TableNameWithPrefix() // 计算总文档数 totalDocsSql := "SELECT COUNT(DISTINCT CONCAT(content_type, '-', content_id)) FROM " + tableName var totalDocs int err := o.Raw(totalDocsSql).QueryRow(&totalDocs) if err != nil { return nil, 0, err } // 构建IN条件 wordPlaceholders := "" wordArgs := make([]any, 0) for i, word := range words { if i > 0 { wordPlaceholders += "," } wordPlaceholders += "?" wordArgs = append(wordArgs, word) } sql := "SELECT word, content_type, content_id, word_count FROM " + tableName + " WHERE word IN (" + wordPlaceholders + ") ORDER BY content_type, content_id" type indexRecord struct { Word string ContentType int ContentId int WordCount int } var records []indexRecord _, err = o.Raw(sql, wordArgs...).QueryRows(&records) if err != nil { return nil, 0, err } // 计算各文档的总词数 sql = "SELECT content_type, content_id, count(word_count) total_word_count FROM " + tableName + " GROUP BY content_type, content_id" type docWordCountRecord struct { ContentType int ContentId int TotalWordCount int } var docWordCountRecords []docWordCountRecord _, err = o.Raw(sql).QueryRows(&docWordCountRecords) if err != nil { return nil, 0, err } docTotalWordCountMap := make(map[string]int) for _, record := range docWordCountRecords { key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) docTotalWordCountMap[key] = record.TotalWordCount } // 聚合每个(content_type, content_id)的词频和计算TF-IDF contentMap := make(map[string]*ContentReverseIndexResult) for _, record := range records { key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) if result, exists := contentMap[key]; exists { result.WordCounts = append(result.WordCounts, record.WordCount) } else { contentMap[key] = &ContentReverseIndexResult{ ContentId: record.ContentId, ContentType: record.ContentType, WordCounts: []int{record.WordCount}, } } } docMapWithWords := make(map[string]int) // 用于计算包含搜索词的文档数 // 计算每个文档包含多少个查询词 docWordCount := make(map[string]int) for _, record := range records { key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) docWordCount[key] += record.WordCount docMapWithWords[key] += 1 } // 计算IDF并生成结果 results := make([]*ContentReverseIndexResult, 0, len(contentMap)) for key := range contentMap { result := contentMap[key] // 计算TF:词频之和 tf := float64(docWordCount[key]) / float64(docTotalWordCountMap[key]+1) // 计算DF:包含该词的文档数(简化处理,使用该文档包含的查询词数量) df := len(docMapWithWords) // 计算IDF idf := 0.0 if df > 0 && totalDocs > 0 { idf = math.Log(float64(totalDocs+1) / float64(df)) } // 用于根据文档总词数调整TF-IDF的权重,避免总词数过小的文档权重过高 alpha := math.Log(1.0+float64(docTotalWordCountMap[key])*0.01) * 100 // TF-IDF分数 result.Score = float64(tf) * idf * float64(alpha) results = append(results, result) } // 按Score降序排序 sortResultsByScore(results) totalCount := len(results) // 分页 offset := (pageIndex - 1) * pageSize start := offset end := offset + pageSize if start > totalCount { start = totalCount } if end > totalCount { end = totalCount } if start >= end { return nil, totalCount, nil } return results[start:end], totalCount, nil } func sortResultsByScore(results []*ContentReverseIndexResult) { for i := 0; i < len(results)-1; i++ { for j := i + 1; j < len(results); j++ { if results[i].Score < results[j].Score { results[i], results[j] = results[j], results[i] } } } } func generateIndexId(contentType, contentId int, word string) string { source := fmt.Sprintf("%d-%d-%s", contentType, contentId, word) hasher := md5.New() hasher.Write([]byte(source)) hash := hasher.Sum(nil) return hex.EncodeToString(hash)[:32] } func BuildIndexForDocument(documentId int, content string) error { if documentId <= 0 { return errors.New("文档ID必须大于0") } index := NewContentReverseIndex() err := index.DeleteByContentTypeAndContentId(1, documentId) if err != nil { logs.Error("删除文档倒排索引失败 ->", documentId, err) return err } words := segmenter.Segment(content) if len(words) == 0 { return nil } wordCountMap := make(map[string]int) for _, word := range words { if len(word) > 64 { word = word[:64] } wordCountMap[word]++ } indices := make([]*ContentReverseIndex, 0, len(wordCountMap)) for word, count := range wordCountMap { id := generateIndexId(1, documentId, word) indexItem := &ContentReverseIndex{ Id: id, Word: word, ContentType: 1, ContentId: documentId, WordCount: count, } indices = append(indices, indexItem) } if len(indices) > 0 { err = index.BatchInsert(indices) if err != nil { return fmt.Errorf("批量插入文档倒排索引失败 -> %d %v", documentId, err) } } return nil } func BuildIndexForBlog(blogId int, content string) error { if blogId <= 0 { return errors.New("BlogID必须大于0") } index := NewContentReverseIndex() err := index.DeleteByContentTypeAndContentId(2, blogId) if err != nil { logs.Error("删除Blog倒排索引失败 ->", blogId, err) return err } words := segmenter.Segment(content) if len(words) == 0 { return nil } wordCountMap := make(map[string]int) for _, word := range words { if len(word) > 64 { word = word[:64] } wordCountMap[word]++ } indices := make([]*ContentReverseIndex, 0, len(wordCountMap)) for word, count := range wordCountMap { id := generateIndexId(2, blogId, word) indexItem := &ContentReverseIndex{ Id: id, Word: word, ContentType: 2, ContentId: blogId, WordCount: count, } indices = append(indices, indexItem) } if len(indices) > 0 { err = index.BatchInsert(indices) if err != nil { logs.Error("批量插入Blog倒排索引失败 ->", blogId, err) return err } } return nil } func CheckDocumentIndexed(documentId int) bool { if documentId <= 0 { return false } o := orm.NewOrm() index := NewContentReverseIndex() return o.QueryTable(index.TableNameWithPrefix()).Filter("content_type", 1).Filter("content_id", documentId).Exist() } func CheckBlogIndexed(blogId int) bool { if blogId <= 0 { return false } o := orm.NewOrm() index := NewContentReverseIndex() return o.QueryTable(index.TableNameWithPrefix()).Filter("content_type", 2).Filter("content_id", blogId).Exist() } func GetUnindexedDocuments(limit int) ([]*Document, error) { o := orm.NewOrm() var documents []*Document docTable := NewDocument().TableNameWithPrefix() indexTable := NewContentReverseIndex().TableNameWithPrefix() sql := "SELECT d.* FROM " + docTable + " d " + "LEFT JOIN " + indexTable + " i ON i.content_type = 1 AND i.content_id = d.document_id " + "WHERE i.id IS NULL " + "ORDER BY d.document_id DESC" if limit > 0 { sql += " LIMIT ?" _, err := o.Raw(sql, limit).QueryRows(&documents) return documents, err } _, err := o.Raw(sql).QueryRows(&documents) return documents, err } func GetUnindexedBlogs(limit int) ([]*Blog, error) { o := orm.NewOrm() var blogs []*Blog blogTable := NewBlog().TableNameWithPrefix() indexTable := NewContentReverseIndex().TableNameWithPrefix() sql := "SELECT b.* FROM " + blogTable + " b " + "LEFT JOIN " + indexTable + " i ON i.content_type = 2 AND i.content_id = b.blog_id " + "WHERE i.id IS NULL " + "ORDER BY b.blog_id DESC" if limit > 0 { sql += " LIMIT ?" _, err := o.Raw(sql, limit).QueryRows(&blogs) return blogs, err } _, err := o.Raw(sql).QueryRows(&blogs) return blogs, err } // InitializeMissingIndexes 初始化缺失的倒排索引 func InitializeMissingIndexes() { go func() { logs.Info("开始检查并初始化缺失的倒排索引...") InitializeMissingDocumentIndexes() InitializeMissingBlogIndexes() logs.Info("倒排索引初始化检查完成") }() } func InitializeMissingDocumentIndexes() { batchSize := 100 for { documents, err := GetUnindexedDocuments(batchSize) if err != nil { logs.Error("获取未索引文档失败 ->", err) break } if len(documents) == 0 { break } for _, doc := range documents { indexed := CheckDocumentIndexed(doc.DocumentId) if !indexed { content := doc.Release if content == "" { content = doc.Markdown } for i := 0; i < 10; i++ { // 标题内容"十分"重要 content = doc.DocumentName + "\n" + content } content = utils.StripTags(content) err := BuildIndexForDocument(doc.DocumentId, content) if err != nil { logs.Error("构建文档倒排索引失败 ->", doc.DocumentId, err) } else { logs.Info("文档倒排索引构建成功 ->", doc.DocumentId) } } } } } func InitializeMissingBlogIndexes() { batchSize := 100 for { blogs, err := GetUnindexedBlogs(batchSize) if err != nil { logs.Error("获取未索引Blog失败 ->", err) break } if len(blogs) == 0 { break } for _, blog := range blogs { indexed := CheckBlogIndexed(blog.BlogId) if !indexed { content := blog.BlogRelease if content == "" { content = blog.BlogContent } for i := 0; i < 10; i++ { // 标题内容"十分"重要 content = blog.BlogTitle + "\n" + content } content = utils.StripTags(content) err := BuildIndexForBlog(blog.BlogId, content) if err != nil { logs.Error("构建Blog倒排索引失败 ->", blog.BlogId, err) } else { logs.Info("Blog倒排索引构建成功 ->", blog.BlogId) } } } } } ================================================ FILE: models/ConvertBookResult.go ================================================ package models // 转换结果 type ConvertBookResult struct { PDFPath string EpubPath string MobiPath string WordPath string } ================================================ FILE: models/Dashboard.go ================================================ package models import "github.com/beego/beego/v2/client/orm" type Dashboard struct { BookNumber int64 `json:"book_number"` DocumentNumber int64 `json:"document_number"` MemberNumber int64 `json:"member_number"` CommentNumber int64 `json:"comment_number"` AttachmentNumber int64 `json:"attachment_number"` } func NewDashboard() *Dashboard { return &Dashboard{} } func (m *Dashboard) Query() *Dashboard { o := orm.NewOrm() book_number, _ := o.QueryTable(NewBook().TableNameWithPrefix()).Count() m.BookNumber = book_number document_count, _ := o.QueryTable(NewDocument().TableNameWithPrefix()).Count() m.DocumentNumber = document_count member_number, _ := o.QueryTable(NewMember().TableNameWithPrefix()).Count() m.MemberNumber = member_number //comment_number,_ := o.QueryTable(NewComment().TableNameWithPrefix()).Count() m.CommentNumber = 0 attachment_number, _ := o.QueryTable(NewAttachment().TableNameWithPrefix()).Count() m.AttachmentNumber = attachment_number return m } ================================================ FILE: models/DocumentHistory.go ================================================ package models import ( "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) type DocumentHistory struct { HistoryId int `orm:"column(history_id);pk;auto;unique" json:"history_id"` Action string `orm:"column(action);size(255);description(modify)" json:"action"` ActionName string `orm:"column(action_name);size(255);description(修改文档)" json:"action_name"` DocumentId int `orm:"column(document_id);type(int);index;description(关联文档id)" json:"doc_id"` DocumentName string `orm:"column(document_name);size(500);description(关联文档id)" json:"doc_name"` ParentId int `orm:"column(parent_id);type(int);index;default(0);description(父级文档id)" json:"parent_id"` Markdown string `orm:"column(markdown);type(text);null;description(文档内容)" json:"markdown"` Content string `orm:"column(content);type(text);null;description(文档内容)" json:"content"` MemberId int `orm:"column(member_id);type(int);description(作者id)" json:"member_id"` ModifyTime time.Time `orm:"column(modify_time);type(datetime);auto_now;description(修改时间)" json:"modify_time"` ModifyAt int `orm:"column(modify_at);type(int);description(修改人id)" json:"-"` Version int64 `orm:"type(bigint);column(version);description(版本)" json:"version"` IsOpen int `orm:"column(is_open);type(int);default(0);description(是否展开子目录 0:阅读时关闭节点 1:阅读时展开节点 2:空目录 单击时会展开下级节点)" json:"is_open"` } type DocumentHistorySimpleResult struct { HistoryId int `json:"history_id"` ActionName string `json:"action_name"` MemberId int `json:"member_id"` Account string `json:"account"` ModifyAt int `json:"modify_at"` ModifyName string `json:"modify_name"` ModifyTime time.Time `json:"modify_time"` Version int64 `json:"version"` } // TableName 获取对应数据库表名. func (m *DocumentHistory) TableName() string { return "document_history" } // TableEngine 获取数据使用的引擎. func (m *DocumentHistory) TableEngine() string { return "INNODB" } func (m *DocumentHistory) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewDocumentHistory() *DocumentHistory { return &DocumentHistory{} } func (m *DocumentHistory) Find(id int) (*DocumentHistory, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("history_id", id).One(m) return m, err } //清空指定文档的历史. func (m *DocumentHistory) Clear(docId int) error { o := orm.NewOrm() _, err := o.Raw("DELETE md_document_history WHERE document_id = ?", docId).Exec() return err } //删除历史. func (m *DocumentHistory) Delete(historyId, docId int) error { o := orm.NewOrm() _, err := o.QueryTable(m.TableNameWithPrefix()).Filter("history_id", historyId).Filter("document_id", docId).Delete() return err } //恢复指定历史的文档. func (m *DocumentHistory) Restore(historyId, docId, uid int) error { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("history_id", historyId).Filter("document_id", docId).One(m) if err != nil { return err } doc, err := NewDocument().Find(m.DocumentId) if err != nil { return err } history := NewDocumentHistory() history.DocumentId = docId history.Content = doc.Content history.Markdown = doc.Markdown history.DocumentName = doc.DocumentName history.ModifyAt = uid history.MemberId = doc.MemberId history.ParentId = doc.ParentId history.Version = time.Now().Unix() history.Action = "restore" history.ActionName = "恢复文档" history.IsOpen = doc.IsOpen history.InsertOrUpdate() doc.DocumentName = m.DocumentName doc.Content = m.Content doc.Markdown = m.Markdown doc.Release = m.Content doc.Version = time.Now().Unix() doc.IsOpen = m.IsOpen _, err = o.Update(doc) return err } func (m *DocumentHistory) InsertOrUpdate() (history *DocumentHistory, err error) { o := orm.NewOrm() history = m if m.HistoryId > 0 { _, err = o.Update(m) } else { _, err = o.Insert(m) if err == nil { if doc, e := NewDocument().Find(m.DocumentId); e == nil { if book, e := NewBook().Find(doc.BookId); e == nil && book.HistoryCount > 0 { //如果已存在的历史记录大于指定的记录,则清除旧记录 if c, e := o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", doc.DocumentId).Count(); e == nil && c > int64(book.HistoryCount) { count := c - int64(book.HistoryCount) logs.Info("需要删除的历史文档数量:", count) var lists []DocumentHistory if _, e := o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", doc.DocumentId).OrderBy("history_id").Limit(count).All(&lists, "history_id"); e == nil { for _, d := range lists { o.Delete(&d) } } } else { logs.Info(book.HistoryCount) } } } } } return } //分页查询指定文档的历史. func (m *DocumentHistory) FindToPager(docId, pageIndex, pageSize int) (docs []*DocumentHistorySimpleResult, totalCount int, err error) { o := orm.NewOrm() offset := (pageIndex - 1) * pageSize totalCount = 0 sql := `SELECT history.*,m1.account,m2.account as modify_name FROM md_document_history AS history LEFT JOIN md_members AS m1 ON history.member_id = m1.member_id LEFT JOIN md_members AS m2 ON history.modify_at = m2.member_id WHERE history.document_id = ? ORDER BY history.history_id DESC limit ? offset ?;` _, err = o.Raw(sql, docId, pageSize, offset).QueryRows(&docs) if err != nil { return } var count int64 count, err = o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", docId).Count() if err != nil { return } totalCount = int(count) return } ================================================ FILE: models/DocumentModel.go ================================================ package models import ( "time" "github.com/beego/i18n" "fmt" "strconv" "bytes" "os" "path/filepath" "strings" "github.com/PuerkitoBio/goquery" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/mindoc-org/mindoc/cache" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" ) // Document struct. type Document struct { DocumentId int `orm:"pk;auto;unique;column(document_id)" json:"doc_id"` DocumentName string `orm:"column(document_name);size(500);description(文档名称)" json:"doc_name"` Identify string `orm:"column(identify);size(100);index;null;default(null);description(唯一标识)" json:"identify"` // Identify 文档唯一标识 BookId int `orm:"column(book_id);type(int);index;description(关联bools表主键)" json:"book_id"` ParentId int `orm:"column(parent_id);type(int);index;default(0);description(父级文档)" json:"parent_id"` OrderSort int `orm:"column(order_sort);default(0);type(int);index;description(排序从小到大排序)" json:"order_sort"` Markdown string `orm:"column(markdown);type(text);null;description(markdown内容)" json:"markdown"` // Markdown markdown格式文档. MarkdownTheme string `orm:"column(markdown_theme);size(50);default(theme__light);description(markdown主题)" json:"markdown_theme"` Release string `orm:"column(release);type(text);null;description(文章内容)" json:"release"` // Release 发布后的Html格式内容. Content string `orm:"column(content);type(text);null;description(文章内容)" json:"content"` // Content 未发布的 Html 格式内容. CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add;description(创建时间)" json:"create_time"` MemberId int `orm:"column(member_id);type(int);description(关系用户id)" json:"member_id"` ModifyTime time.Time `orm:"column(modify_time);type(datetime);auto_now;description(修改时间)" json:"modify_time"` ModifyAt int `orm:"column(modify_at);type(int);description(修改人id)" json:"-"` Version int64 `orm:"column(version);type(bigint);description(版本,关联历史文档里的version)" json:"version"` IsOpen int `orm:"column(is_open);type(int);default(0);description(是否展开子目录 0:阅读时关闭节点 1:阅读时展开节点 2:空目录 单击时会展开下级节点)" json:"is_open"` //是否展开子目录:0 否/1 是 /2 空间节点,单击时展开下一级 ViewCount int `orm:"column(view_count);type(int);description(浏览量)" json:"view_count"` AttachList []*Attachment `orm:"-" json:"attach"` //i18n Lang string `orm:"-"` } // 多字段唯一键 func (item *Document) TableUnique() [][]string { return [][]string{{"book_id", "identify"}} } // TableName 获取对应数据库表名. func (item *Document) TableName() string { return "documents" } // TableEngine 获取数据使用的引擎. func (item *Document) TableEngine() string { return "INNODB" } func (item *Document) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + item.TableName() } func NewDocument() *Document { return &Document{ Version: time.Now().Unix(), } } // 根据文档ID查询指定文档. func (item *Document) Find(id int) (*Document, error) { if id <= 0 { return item, ErrInvalidParameter } o := orm.NewOrm() err := o.QueryTable(item.TableNameWithPrefix()).Filter("document_id", id).One(item) if err == orm.ErrNoRows { return item, ErrDataNotExist } return item, nil } // 插入和更新文档. func (item *Document) InsertOrUpdate(cols ...string) error { o := orm.NewOrm() item.DocumentName = utils.StripTags(item.DocumentName) var err error if item.DocumentId > 0 { _, err = o.Update(item, cols...) } else { if item.Identify == "" { book := NewBook() identify := "docs" if err := o.QueryTable(book.TableNameWithPrefix()).Filter("book_id", item.BookId).One(book, "identify"); err == nil { identify = book.Identify } item.Identify = fmt.Sprintf("%s-%s", identify, strconv.FormatInt(time.Now().UnixNano(), 32)) } if item.OrderSort == 0 { sort, _ := o.QueryTable(item.TableNameWithPrefix()).Filter("book_id", item.BookId).Filter("parent_id", item.ParentId).Count() item.OrderSort = int(sort) + 1 } _, err = o.Insert(item) NewBook().ResetDocumentNumber(item.BookId) } if err != nil { return err } return nil } // 根据文档识别编号和项目id获取一篇文档 func (item *Document) FindByIdentityFirst(identify string, bookId int) (*Document, error) { o := orm.NewOrm() err := o.QueryTable(item.TableNameWithPrefix()).Filter("book_id", bookId).Filter("identify", identify).One(item) return item, err } // 递归删除一个文档. func (item *Document) RecursiveDocument(docId int) error { o := orm.NewOrm() if doc, err := item.Find(docId); err == nil { // 删除文档的倒排索引 index := NewContentReverseIndex() _ = index.DeleteByContentTypeAndContentId(1, docId) o.Delete(doc) NewDocumentHistory().Clear(doc.DocumentId) } var maps []orm.Params _, err := o.Raw("SELECT document_id FROM " + item.TableNameWithPrefix() + " WHERE parent_id=" + strconv.Itoa(docId)).Values(&maps) if err != nil { logs.Error("RecursiveDocument => ", err) return err } for _, param := range maps { if docId, ok := param["document_id"].(string); ok { id, _ := strconv.Atoi(docId) // 删除子文档的倒排索引 index := NewContentReverseIndex() _ = index.DeleteByContentTypeAndContentId(1, id) o.QueryTable(item.TableNameWithPrefix()).Filter("document_id", id).Delete() item.RecursiveDocument(id) } } return nil } // 将文档写入缓存 func (item *Document) PutToCache() { go func(m Document) { if m.Identify == "" { if err := cache.Put("Document.Id."+strconv.Itoa(m.DocumentId), m, time.Second*3600); err != nil { logs.Info("文档缓存失败:", m.DocumentId) } } else { if err := cache.Put(fmt.Sprintf("Document.BookId.%d.Identify.%s", m.BookId, m.Identify), m, time.Second*3600); err != nil { logs.Info("文档缓存失败:", m.DocumentId) } } }(*item) } // 清除缓存 func (item *Document) RemoveCache() { go func(m Document) { cache.Put("Document.Id."+strconv.Itoa(m.DocumentId), m, time.Second*3600) if m.Identify != "" { cache.Put(fmt.Sprintf("Document.BookId.%d.Identify.%s", m.BookId, m.Identify), m, time.Second*3600) } }(*item) } // 从缓存获取 func (item *Document) FromCacheById(id int) (*Document, error) { if err := cache.Get("Document.Id."+strconv.Itoa(id), &item); err == nil && item.DocumentId > 0 { logs.Info("从缓存中获取文档信息成功 ->", item.DocumentId) return item, nil } if item.DocumentId > 0 { item.PutToCache() } item, err := item.Find(id) if err == nil { item.PutToCache() } return item, err } // 根据文档标识从缓存中查询文档 func (item *Document) FromCacheByIdentify(identify string, bookId int) (*Document, error) { key := fmt.Sprintf("Document.BookId.%d.Identify.%s", bookId, identify) if err := cache.Get(key, item); err == nil && item.DocumentId > 0 { logs.Info("从缓存中获取文档信息成功 ->", key) return item, nil } defer func() { if item.DocumentId > 0 { item.PutToCache() } }() return item.FindByIdentityFirst(identify, bookId) } // 根据项目ID查询文档列表. func (item *Document) FindListByBookId(bookId int) (docs []*Document, err error) { o := orm.NewOrm() _, err = o.QueryTable(item.TableNameWithPrefix()).Filter("book_id", bookId).OrderBy("order_sort").All(&docs) return } // 判断文章是否存在 func (item *Document) IsExist(documentId int) bool { o := orm.NewOrm() return o.QueryTable(item.TableNameWithPrefix()).Filter("document_id", documentId).Exist() } // 发布单篇文档 func (item *Document) ReleaseContent() error { item.Release = strings.TrimSpace(item.Content) err := item.Processor().InsertOrUpdate("release") if err != nil { logs.Error(fmt.Sprintf("发布失败 -> %+v", item), err) return err } //当文档发布后,需要清除已缓存的转换文档和文档缓存 item.RemoveCache() if err := os.RemoveAll(filepath.Join(conf.WorkingDirectory, "uploads", "books", strconv.Itoa(item.BookId))); err != nil { logs.Error("删除已缓存的文档目录失败 -> ", filepath.Join(conf.WorkingDirectory, "uploads", "books", strconv.Itoa(item.BookId))) return err } // 刷新倒排索引 go func(docId int, docName, release, markdown string) { content := docName + "\n" + release if content == "" { content = markdown } content = utils.StripTags(content) if err := BuildIndexForDocument(docId, content); err != nil { logs.Error("error: 构建文档倒排索引失败 ->", docId, err) } }(item.DocumentId, item.DocumentName, item.Release, item.Markdown) return nil } // Processor 调用位置两处: // 1. 项目发布和文档发布: 处理文档的外链,附件,底部编辑信息等; // 2. 文档阅读:可以修复存在问题的文档,使其能正常显示附件下载和文档作者信息等。 func (item *Document) Processor() *Document { if item.Release != "" { item.Release = utils.SafetyProcessor(item.Release) } else { // Release内容为空,直接赋值文档标签,保证附件下载正常 item.Release = "
" } // Next: 生成文档的一些附加信息 if docQuery, err := goquery.NewDocumentFromReader(bytes.NewBufferString(item.Release)); err == nil { //处理附件 if selector := docQuery.Find("div.attach-list").First(); selector.Size() <= 0 { //处理附件 attachList, err := NewAttachment().FindListByDocumentId(item.DocumentId) if err == nil && len(attachList) > 0 { content := bytes.NewBufferString("
" + i18n.Tr(item.Lang, "doc.attachment") + "
    ") for _, attach := range attachList { if strings.HasPrefix(attach.HttpPath, "/") { attach.HttpPath = strings.TrimSuffix(conf.BaseUrl, "/") + attach.HttpPath } li := fmt.Sprintf("
  • %s
  • ", attach.HttpPath, attach.FileName, attach.FileName) content.WriteString(li) } content.WriteString("
") if docQuery == nil { docQuery, err = goquery.NewDocumentFromReader(content) if err != nil { logs.Error("goquery->NewDocumentFromReader err:%+v", err) } } else { if selector := docQuery.Find("div.wiki-bottom").First(); selector.Size() > 0 { selector.BeforeHtml(content.String()) //This branch should be a compatible branch. } else if selector := docQuery.Find("div.markdown-article").First(); selector.Size() > 0 { selector.AppendHtml(content.String()) //The document produced by the editor of Markdown will have this tag.class. } else if selector := docQuery.Find("div.whole-article-wrap").First(); selector.Size() > 0 { selector.AppendHtml(content.String()) //All documents should have this tag. } } } } //处理了文档底部信息 if selector := docQuery.Find("div.wiki-bottom").First(); selector.Size() <= 0 && item.MemberId > 0 { //处理文档结尾信息 docCreator, err := NewMember().Find(item.MemberId, "real_name", "account") release := "
" release += i18n.Tr(item.Lang, "doc.ft_author") if err == nil && docCreator != nil { if docCreator.RealName != "" { release += docCreator.RealName } else { release += docCreator.Account } } release += "  " + i18n.Tr(item.Lang, "doc.ft_create_time") + item.CreateTime.Local().Format("2006-01-02 15:04") + "
" if item.ModifyAt > 0 { docModify, err := NewMember().Find(item.ModifyAt, "real_name", "account") if err == nil { if docModify.RealName != "" { release += i18n.Tr(item.Lang, "doc.ft_last_editor") + docModify.RealName } else { release += i18n.Tr(item.Lang, "doc.ft_last_editor") + docModify.Account } } } release += "  " + i18n.Tr(item.Lang, "doc.ft_update_time") + item.ModifyTime.Local().Format("2006-01-02 15:04") + "
" release += "
" if selector := docQuery.Find("div.markdown-article").First(); selector.Size() > 0 { selector.AppendHtml(release) } else if selector := docQuery.Find("div.whole-article-wrap").First(); selector.Size() > 0 { selector.AppendHtml(release) } } cdnimg, _ := web.AppConfig.String("cdnimg") docQuery.Find("img").Each(func(i int, selection *goquery.Selection) { if src, ok := selection.Attr("src"); ok { src = strings.TrimSpace(strings.ToLower(src)) //过滤掉没有链接的图片标签 if src == "" || strings.HasPrefix(src, "data:text/html") { selection.Remove() return } //设置图片为CDN地址 if cdnimg != "" && strings.HasPrefix(src, "/uploads/") { selection.SetAttr("src", utils.JoinURI(cdnimg, src)) } } selection.RemoveAttr("onerror").RemoveAttr("onload") }) //过滤A标签的非法连接 docQuery.Find("a").Each(func(i int, selection *goquery.Selection) { if val, exists := selection.Attr("href"); exists { if val == "" { selection.SetAttr("href", "#") return } val = strings.Replace(strings.ToLower(val), " ", "", -1) //移除危险脚本链接 if strings.HasPrefix(val, "data:text/html") || strings.HasPrefix(val, "vbscript:") || strings.HasPrefix(val, "javascript:") || strings.HasPrefix(val, "javascript:") { selection.SetAttr("href", "#") } } //移除所有 onerror 属性 selection.RemoveAttr("onerror").RemoveAttr("onload").RemoveAttr("onclick") }) docQuery.Find("script").Remove() docQuery.Find("link").Remove() docQuery.Find("vbscript").Remove() if html, err := docQuery.Html(); err == nil { item.Release = strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(html), ""), "") } } return item } // 增加阅读次数 func (item *Document) IncrViewCount(id int) { o := orm.NewOrm() o.QueryTable(item.TableNameWithPrefix()).Filter("document_id", id).Update(orm.Params{ "view_count": orm.ColValue(orm.ColAdd, 1), }) } ================================================ FILE: models/DocumentSearchResult.go ================================================ package models import ( "fmt" "regexp" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" ) type DocumentSearchResult struct { DocumentId int `json:"doc_id"` DocumentName string `json:"doc_name"` // Identify 文档唯一标识 Identify string `json:"identify"` Description string `json:"description"` Author string `json:"author"` ModifyTime time.Time `json:"modify_time"` CreateTime time.Time `json:"create_time"` BookId int `json:"book_id"` BookName string `json:"book_name"` BookIdentify string `json:"book_identify"` SearchType string `json:"search_type"` } var escape_re = regexp.MustCompile(`(?mi)(\bLIKE\s+\?)`) var escape_replace = "${1} ESCAPE '\\'" func need_escape(keyword string) bool { dbadapter, _ := web.AppConfig.String("db_adapter") if strings.EqualFold(dbadapter, "sqlite3") && (strings.Contains(keyword, "\\_") || strings.Contains(keyword, "\\%")) { return true } return false } func escape_name(name string) string { dbadapter, _ := web.AppConfig.String("db_adapter") ch := "`" if strings.EqualFold(dbadapter, "postgres") { ch = `"` } return fmt.Sprintf("%s%s%s", ch, name, ch) } func NewDocumentSearchResult() *DocumentSearchResult { return &DocumentSearchResult{} } // 分页全局搜索. func (m *DocumentSearchResult) FindToPager(keyword string, pageIndex, pageSize, memberId int) (searchResult []*DocumentSearchResult, totalCount int, err error) { o := orm.NewOrm() offset := (pageIndex - 1) * pageSize keyword = "%" + strings.Replace(keyword, " ", "%", -1) + "%" _need_escape := need_escape(keyword) escape_sql := func(sql string) string { if _need_escape { return escape_re.ReplaceAllString(sql, escape_replace) } return sql } if memberId <= 0 { sql1 := `SELECT count(doc.document_id) as total_count FROM md_documents AS doc LEFT JOIN md_books as book ON doc.book_id = book.book_id WHERE book.privately_owned = 0 AND (doc.document_name LIKE ? OR doc.release LIKE ?) ` sql2 := `SELECT * FROM ( SELECT doc.document_id, doc.modify_time, doc.create_time, doc.document_name, doc.identify, doc.release AS description, book.identify AS book_identify, book.book_name, rel.member_id, mdmb.account AS author, 'document' AS search_type FROM md_documents AS doc LEFT JOIN md_books AS book ON doc.book_id = book.book_id LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id WHERE book.privately_owned = 0 AND (doc.document_name LIKE ? OR doc.release LIKE ?) UNION ALL SELECT book.book_id AS document_id, book.modify_time, book.create_time, book.book_name AS document_name, book.identify, book.description, book.identify AS book_identify, book.book_name, rel.member_id, mdmb.account AS author, 'book' AS search_type FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id WHERE book.privately_owned = 0 AND (book.book_name LIKE ? OR book.description LIKE ?) UNION ALL SELECT blog.blog_id AS document_id, blog.modify_time, blog.create_time, blog.blog_title as document_name, blog.blog_identify, blog.blog_release, blog.blog_identify, blog.blog_title as book_name, blog.member_id, mdmb.account, 'blog' AS search_type FROM md_blogs AS blog LEFT JOIN md_members AS mdmb ON blog.member_id = mdmb.member_id WHERE blog.blog_status = 'public' AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?) ) AS union_table ORDER BY create_time DESC LIMIT ? OFFSET ?;` err = o.Raw(escape_sql(sql1), keyword, keyword).QueryRow(&totalCount) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } sql3 := ` SELECT count(*) FROM md_blogs AS blog WHERE blog.blog_status = 'public' AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?);` c := 0 err = o.Raw(escape_sql(sql3), keyword, keyword).QueryRow(&c) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } totalCount += c //查询项目的数量 sql4 := `SELECT count(*) as total_count FROM md_books as book WHERE book.privately_owned = 0 AND (book.book_name LIKE ? OR book.description LIKE ?);` err = o.Raw(escape_sql(sql4), keyword, keyword).QueryRow(&c) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } totalCount += c _, err = o.Raw(escape_sql(sql2), keyword, keyword, keyword, keyword, keyword, keyword, pageSize, offset).QueryRows(&searchResult) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } } else { sql1 := `SELECT count(doc.document_id) as total_count FROM md_documents AS doc LEFT JOIN md_books as book ON doc.book_id = book.book_id LEFT JOIN md_relationship AS rel ON doc.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_relationship AS rel1 ON doc.book_id = rel1.book_id AND rel1.member_id = ? left join (select * from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.role_id,t.team_member_id,t.book_id) as team on team.book_id = book.book_id WHERE (book.privately_owned = 0 OR rel1.relationship_id > 0 or team.team_member_id > 0) AND (doc.document_name LIKE ? OR doc.release LIKE ?);` sql2 := `SELECT * FROM ( SELECT doc.document_id, doc.modify_time, doc.create_time, doc.document_name, doc.identify, doc.release AS description, book.identify AS book_identify, book.book_name, rel.member_id, mdmb.account AS author, 'document' AS search_type FROM md_documents AS doc LEFT JOIN md_books AS book ON doc.book_id = book.book_id LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id LEFT JOIN md_relationship AS rel1 ON doc.book_id = rel1.book_id AND rel1.member_id = ? LEFT JOIN (SELECT * FROM (SELECT book_id, team_member_id, role_id FROM md_team_relationship AS mtr LEFT JOIN md_team_member AS mtm ON mtm.team_id = mtr.team_id AND mtm.member_id = ? ORDER BY role_id DESC) AS t GROUP BY t.role_id, t.team_member_id, t.book_id) AS team ON team.book_id = book.book_id WHERE (book.privately_owned = 0 OR rel1.relationship_id > 0 OR team.team_member_id > 0) AND (doc.document_name LIKE ? OR doc.release LIKE ?) UNION ALL SELECT book.book_id AS document_id, book.modify_time, book.create_time, book.book_name AS document_name, book.identify, book.description AS description, book.identify AS book_identify, book.book_name, rel.member_id, mdmb.account AS author, 'book' AS search_type FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id LEFT JOIN md_relationship AS rel1 ON book.book_id = rel1.book_id AND rel1.member_id = ? LEFT JOIN (SELECT * FROM (SELECT book_id, team_member_id, role_id FROM md_team_relationship AS mtr LEFT JOIN md_team_member AS mtm ON mtm.team_id = mtr.team_id AND mtm.member_id = ? ORDER BY role_id DESC) AS t GROUP BY t.role_id, t.team_member_id, t.book_id) AS team ON team.book_id = book.book_id WHERE (book.privately_owned = 0 OR rel1.relationship_id > 0 OR team.team_member_id > 0) AND (book.book_name LIKE ? OR book.description LIKE ?) UNION ALL SELECT blog.blog_id AS document_id, blog.modify_time, blog.create_time, blog.blog_title as document_name, blog.blog_identify as identify, blog.blog_release as description, blog.blog_identify AS book_identify, blog.blog_title as book_name, blog.member_id, mdmb.account, 'blog' AS search_type FROM md_blogs AS blog LEFT JOIN md_members AS mdmb ON blog.member_id = mdmb.member_id WHERE (blog.blog_status = 'public' OR blog.member_id = ?) AND blog.blog_type = 0 AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?) ) AS union_table ORDER BY create_time DESC LIMIT ? OFFSET ?;` err = o.Raw(escape_sql(sql1), memberId, memberId, keyword, keyword).QueryRow(&totalCount) if err != nil { return } sql3 := ` SELECT count(*) FROM md_blogs AS blog WHERE (blog.blog_status = 'public' OR blog.member_id = ?) AND blog.blog_type = 0 AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?);` c := 0 err = o.Raw(escape_sql(sql3), memberId, keyword, keyword).QueryRow(&c) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } totalCount += c sql4 := `SELECT count(*) as total_count FROM md_books as book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_relationship AS rel1 ON book.book_id = rel1.book_id AND rel1.member_id = ? left join (select * from (select book_id,team_member_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.role_id,t.team_member_id,t.book_id) as team on team.book_id = book.book_id WHERE (book.privately_owned = 0 OR rel1.relationship_id > 0 or team.team_member_id > 0) AND (book.book_name LIKE ? OR book.description LIKE ?);` err = o.Raw(escape_sql(sql4), memberId, memberId, keyword, keyword).QueryRow(&c) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return } totalCount += c _, err = o.Raw(escape_sql(sql2), memberId, memberId, keyword, keyword, memberId, memberId, keyword, keyword, memberId, keyword, keyword, pageSize, offset).QueryRows(&searchResult) if err != nil { return } } return } // 项目内搜索. func (m *DocumentSearchResult) SearchDocument(keyword string, bookId int) (docs []*DocumentSearchResult, err error) { o := orm.NewOrm() sql := fmt.Sprintf("SELECT * FROM md_documents WHERE book_id = ? AND (document_name LIKE ? OR %s LIKE ?) ", escape_name("release")) keyword = "%" + keyword + "%" _need_escape := need_escape(keyword) escape_sql := func(sql string) string { if _need_escape { return escape_re.ReplaceAllString(sql, escape_replace) } return sql } _, err = o.Raw(escape_sql(sql), bookId, keyword, keyword).QueryRows(&docs) return } // 所有项目搜索. func (m *DocumentSearchResult) SearchAllDocument(keyword string) (docs []*DocumentSearchResult, err error) { o := orm.NewOrm() sql := fmt.Sprintf("SELECT * FROM md_documents WHERE (document_name LIKE ? OR %s LIKE ?) ", escape_name("release")) keyword = "%" + keyword + "%" _need_escape := need_escape(keyword) escape_sql := func(sql string) string { if _need_escape { return escape_re.ReplaceAllString(sql, escape_replace) } return sql } _, err = o.Raw(escape_sql(sql), keyword, keyword).QueryRows(&docs) return } ================================================ FILE: models/DocumentTree.go ================================================ package models import ( "bytes" "fmt" "html/template" "math" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" // "gorm.io/driver/sqlite" // "gorm.io/gorm" ) type DocumentTree struct { DocumentId int `json:"id"` DocumentName string `json:"text"` ParentId interface{} `json:"parent"` Identify string `json:"identify"` BookIdentify string `json:"-"` Version int64 `json:"version"` State *DocumentSelected `json:"-"` AAttrs map[string]interface{} `json:"a_attr"` Children []*DocumentTree `json:"children"` } // type DocumentTreeJson struct { // gorm.Model // DocumentId int `json:"id"` // DocumentName string `json:"text"` // ParentId interface{} `json:"parent"` // Children []*DocumentTreeJson `json:"children" gorm:"-"` // } type DocumentSelected struct { Selected bool `json:"selected"` Opened bool `json:"opened"` Disabled bool `json:"disabled"` } // 获取项目的文档树状结构 func (item *Document) FindDocumentTree(bookId int) ([]*DocumentTree, error) { o := orm.NewOrm() trees := make([]*DocumentTree, 0) var docs []*Document count, err := o.QueryTable(item).Filter("book_id", bookId). OrderBy("order_sort", "document_id"). Limit(math.MaxInt32). All(&docs, "document_id", "version", "document_name", "parent_id", "identify", "is_open") if err != nil { return trees, err } book, _ := NewBook().Find(bookId) trees = make([]*DocumentTree, count) for index, item := range docs { tree := &DocumentTree{ AAttrs: map[string]interface{}{"is_open": false, "opened": 0}, } if index == 0 { tree.State = &DocumentSelected{Selected: true, Opened: true} tree.AAttrs = map[string]interface{}{"is_open": true, "opened": 1} } else if item.IsOpen == 1 { tree.State = &DocumentSelected{Selected: false, Opened: true} tree.AAttrs = map[string]interface{}{"is_open": true, "opened": 1} } if item.IsOpen == 2 { tree.State = &DocumentSelected{Selected: false, Opened: false, Disabled: true} tree.AAttrs = map[string]interface{}{"disabled": true, "opened": 2} } tree.DocumentId = item.DocumentId tree.Identify = item.Identify tree.Version = item.Version tree.BookIdentify = book.Identify if item.ParentId > 0 { tree.ParentId = item.ParentId } else { tree.ParentId = "#" } tree.DocumentName = item.DocumentName trees[index] = tree } return trees, nil } // 获取项目的文档树状结构2 func (item *Document) FindDocumentTree2(bookId int) ([]*DocumentTree, error) { o := orm.NewOrm() trees := make([]*DocumentTree, 0) var docs []*Document count, err := o.QueryTable(item).Filter("book_id", bookId). OrderBy("order_sort", "document_id"). Limit(math.MaxInt32). All(&docs, "document_id", "version", "document_name", "parent_id", "identify", "is_open") if err != nil { return trees, err } book, _ := NewBook().Find(bookId) trees = make([]*DocumentTree, count) for index, item := range docs { tree := &DocumentTree{ AAttrs: map[string]interface{}{"is_open": false, "opened": 0}, } if index == 0 { tree.State = &DocumentSelected{Selected: true, Opened: true} tree.AAttrs = map[string]interface{}{"is_open": true, "opened": 1} } else if item.IsOpen == 1 { tree.State = &DocumentSelected{Selected: false, Opened: true} tree.AAttrs = map[string]interface{}{"is_open": true, "opened": 1} } if item.IsOpen == 2 { tree.State = &DocumentSelected{Selected: false, Opened: false, Disabled: true} tree.AAttrs = map[string]interface{}{"disabled": true, "opened": 2} } tree.DocumentId = item.DocumentId tree.Identify = item.Identify tree.Version = item.Version tree.BookIdentify = book.Identify // if item.ParentId > 0 { tree.ParentId = item.ParentId // } else { // tree.ParentId = "#" // } tree.DocumentName = item.DocumentName trees[index] = tree } return trees, nil } func (item *Document) CreateDocumentTreeForHtml(bookId, selectedId int) (string, error) { trees, err := item.FindDocumentTree(bookId) if err != nil { return "", err } parentId := getSelectedNode(trees, selectedId) buf := bytes.NewBufferString("") getDocumentTree(trees, 0, selectedId, parentId, buf) return buf.String(), nil } // 使用递归的方式获取指定ID的顶级ID func getSelectedNode(array []*DocumentTree, parent_id int) int { for _, item := range array { if _, ok := item.ParentId.(string); ok && item.DocumentId == parent_id { return item.DocumentId } else if pid, ok := item.ParentId.(int); ok && item.DocumentId == parent_id { return getSelectedNode(array, pid) } } return 0 } func getDocumentTree(array []*DocumentTree, parentId int, selectedId int, selectedParentId int, buf *bytes.Buffer) { buf.WriteString("
    ") for _, item := range array { pid := 0 if p, ok := item.ParentId.(int); ok { pid = p } if pid == parentId { selected := "" if item.DocumentId == selectedId { selected = ` class="jstree-clicked"` } selectedLi := "" if item.DocumentId == selectedParentId || (item.State != nil && item.State.Opened) { selectedLi = ` class="jstree-open"` } buf.WriteString(fmt.Sprintf("
  • %s", item.Version, selected, template.HTMLEscapeString(item.DocumentName))) for _, sub := range array { if p, ok := sub.ParentId.(int); ok && p == item.DocumentId { getDocumentTree(array, p, selectedId, selectedParentId, buf) break } } buf.WriteString("
  • ") } } buf.WriteString("
") } ================================================ FILE: models/Errors.go ================================================ // Package models 为项目所需的模型对象定义. package models import "errors" var ( // ErrMemberNoExist 用户不存在. ErrMemberNoExist = errors.New("用户不存在") ErrMemberExist = errors.New("用户已存在") ErrMemberDisabled = errors.New("用户被禁用") ErrMemberEmailEmpty = errors.New("用户邮箱不能为空") ErrMemberEmailExist = errors.New("用户邮箱已被使用") ErrMemberDescriptionTooLong = errors.New("用户描述必须小于500字") ErrMemberEmailFormatError = errors.New("邮箱格式不正确") ErrMemberPasswordFormatError = errors.New("密码必须在6-50个字符之间") ErrMemberAccountFormatError = errors.New("账号只能由英文字母数字组成,且在3-50个字符") ErrMemberRoleError = errors.New("用户权限不正确") // ErrorMemberPasswordError 密码错误. ErrorMemberPasswordError = errors.New("用户密码错误") //ErrorMemberAuthMethodInvalid 不支持此认证方式 ErrMemberAuthMethodInvalid = errors.New("不支持此认证方式") //ErrHTTPServerFail ErrHTTPServerFail = errors.New("系统内部异常") //ErrLDAPConnect 无法连接到LDAP服务器 ErrLDAPConnect = errors.New("无法连接到LDAP服务器") //ErrLDAPFirstBind 第一次LDAP绑定失败 ErrLDAPFirstBind = errors.New("第一次LDAP绑定失败") //ErrLDAPSearch LDAP搜索失败 ErrLDAPSearch = errors.New("LDAP搜索失败") //ErrLDAPUserNotFoundOrTooMany ErrLDAPUserNotFoundOrTooMany = errors.New("LDAP用户不存在或者多于一个") // ErrDataNotExist 指定的服务已存在. ErrDataNotExist = errors.New("数据不存在") // ErrInvalidParameter 参数错误. ErrInvalidParameter = errors.New("Invalid parameter") ErrPermissionDenied = errors.New("Permission denied") ErrCommentClosed = errors.New("评论已关闭") ErrCommentContentNotEmpty = errors.New("评论内容不能为空") ) type Error struct { code int message string } func (e Error) Error() string { return e.message } func (e Error) Code() int { return e.code } func NewError(code int, message string) Error { return Error{code: code, message: message} } ================================================ FILE: models/Itemsets.go ================================================ package models import ( "errors" "strings" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" "github.com/mindoc-org/mindoc/utils/cryptil" ) //项目空间 type Itemsets struct { ItemId int `orm:"column(item_id);pk;auto;unique" json:"item_id"` ItemName string `orm:"column(item_name);size(500);description(项目空间名称)" json:"item_name"` ItemKey string `orm:"column(item_key);size(100);unique;description(项目空间标识)" json:"item_key"` Description string `orm:"column(description);type(text);null;description(描述)" json:"description"` MemberId int `orm:"column(member_id);size(100);description(所属用户)" json:"member_id"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add;description(创建时间)" json:"create_time"` ModifyTime time.Time `orm:"column(modify_time);type(datetime);null;auto_now;description(修改时间)" json:"modify_time"` ModifyAt int `orm:"column(modify_at);type(int);description(修改人id)" json:"modify_at"` BookNumber int `orm:"-" json:"book_number"` CreateTimeString string `orm:"-" json:"create_time_string"` CreateName string `orm:"-" json:"create_name"` } // TableName 获取对应数据库表名. func (item *Itemsets) TableName() string { return "itemsets" } // TableEngine 获取数据使用的引擎. func (item *Itemsets) TableEngine() string { return "INNODB" } func (item *Itemsets) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + item.TableName() } func (item *Itemsets) QueryTable() orm.QuerySeter { return orm.NewOrm().QueryTable(item.TableNameWithPrefix()) } func NewItemsets() *Itemsets { return &Itemsets{} } func (item *Itemsets) First(itemId int) (*Itemsets, error) { if itemId <= 0 { return nil, ErrInvalidParameter } err := item.QueryTable().Filter("item_id", itemId).One(item) if err != nil { logs.Error("查询项目空间失败 -> item_id=", itemId, err) } else { item.Include() } return item, err } func (item *Itemsets) FindFirst(itemKey string) (*Itemsets, error) { err := item.QueryTable().Filter("item_key", itemKey).One(item) if err != nil { logs.Error("查询项目空间失败 -> itemKey=", itemKey, err) } else { item.Include() } return item, err } func (item *Itemsets) Exist(itemId int) bool { return item.QueryTable().Filter("item_id", itemId).Exist() } //保存 func (item *Itemsets) Save() (err error) { item.ItemName = strings.TrimSpace(utils.StripTags(item.ItemName)) item.Description = strings.TrimSpace(utils.StripTags(item.Description)) item.ItemKey = strings.TrimSpace(item.ItemKey) if item.ItemName == "" { return errors.New("项目空间名称不能为空") } if item.ItemKey == "" { item.ItemKey = cryptil.NewRandChars(16) } if item.QueryTable().Filter("item_id__ne", item.ItemId).Filter("item_key", item.ItemKey).Exist() { return errors.New("项目空间标识已存在") } if item.ItemId > 0 { _, err = orm.NewOrm().Update(item) } else { _, err = orm.NewOrm().Insert(item) } return } //删除. func (item *Itemsets) Delete(itemId int) (err error) { if itemId <= 0 { return ErrInvalidParameter } if itemId == 1 { return errors.New("默认项目空间不能删除") } if !item.Exist(itemId) { return errors.New("项目空间不存在") } ormer := orm.NewOrm() o, err := ormer.Begin() if err != nil { logs.Error("开启事物失败 ->", err) return err } _, err = o.QueryTable(item.TableNameWithPrefix()).Filter("item_id", itemId).Delete() if err != nil { logs.Error("删除项目空间失败 -> item_id=", itemId, err) o.Rollback() } _, err = o.Raw("update md_books set item_id=1 where item_id=?;", itemId).Exec() if err != nil { logs.Error("删除项目空间失败 -> item_id=", itemId, err) o.Rollback() } return o.Commit() } func (item *Itemsets) Include() (*Itemsets, error) { item.CreateTimeString = item.CreateTime.Format("2006-01-02 15:04:05") if item.MemberId > 0 { if m, err := NewMember().Find(item.MemberId, "account", "real_name"); err == nil { if m.RealName != "" { item.CreateName = m.RealName } else { item.CreateName = m.Account } } } i, err := NewBook().QueryTable().Filter("item_id", item.ItemId).Count() if err != nil && err != orm.ErrNoRows { return item, err } item.BookNumber = int(i) return item, nil } //分页查询. func (item *Itemsets) FindToPager(pageIndex, pageSize int) (list []*Itemsets, totalCount int, err error) { offset := (pageIndex - 1) * pageSize _, err = item.QueryTable().OrderBy("-item_id").Offset(offset).Limit(pageSize).All(&list) if err != nil { return } c, err := item.QueryTable().Count() if err != nil { return } totalCount = int(c) for _, item := range list { item.Include() } return } //根据项目空间名称查询. func (item *Itemsets) FindItemsetsByName(name string, limit int) (*SelectMemberResult, error) { result := SelectMemberResult{} var itemsets []*Itemsets var err error if name == "" { _, err = item.QueryTable().Limit(limit).All(&itemsets) } else { _, err = item.QueryTable().Filter("item_name__icontains", name).Limit(limit).All(&itemsets) } if err != nil { logs.Error("查询项目空间失败 ->", err) return &result, err } items := make([]KeyValueItem, 0) for _, m := range itemsets { item := KeyValueItem{} item.Id = m.ItemId item.Text = m.ItemName items = append(items, item) } result.Result = items return &result, err } //根据项目空间标识查询项目空间的项目列表. func (item *Itemsets) FindItemsetsByItemKey(key string, pageIndex, pageSize, memberId int) (books []*BookResult, totalCount int, err error) { o := orm.NewOrm() err = item.QueryTable().Filter("item_key", key).One(item) if err != nil { logs.Error("查询项目空间时出错 ->", key, err) return nil, 0, err } offset := (pageIndex - 1) * pageSize //如果是登录用户 if memberId > 0 { sql1 := `SELECT COUNT(*) FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select book_id,min(role_id) as role_id from (select book_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by book_id) as team on team.book_id = book.book_id WHERE book.item_id = ? AND (book.privately_owned = 0 or rel.role_id >= 0 or team.role_id >= 0)` err = o.Raw(sql1, memberId, memberId, item.ItemId).QueryRow(&totalCount) if err != nil { logs.Error("查询项目空间时出错 ->", key, err) return } sql2 := `SELECT book.*,rel1.*,mdmb.account AS create_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? left join (select book_id,min(role_id) as role_id from (select book_id,role_id from md_team_relationship as mtr left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc ) as t group by book_id) as team on team.book_id = book.book_id LEFT JOIN md_relationship AS rel1 ON rel1.book_id = book.book_id AND rel1.role_id = 0 LEFT JOIN md_members AS mdmb ON rel1.member_id = mdmb.member_id WHERE book.item_id = ? AND (book.privately_owned = 0 or rel.role_id >= 0 or team.role_id >= 0) ORDER BY order_index desc,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql2, memberId, memberId, item.ItemId, pageSize, offset).QueryRows(&books) return } else { count, err1 := o.QueryTable(NewBook().TableNameWithPrefix()).Filter("privately_owned", 0).Filter("item_id", item.ItemId).Count() if err1 != nil { err = err1 return } totalCount = int(count) sql := `SELECT book.*,rel.*,mdmb.account AS create_name FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id WHERE book.item_id = ? AND book.privately_owned = 0 ORDER BY order_index desc,book.book_id DESC limit ? offset ?` _, err = o.Raw(sql, item.ItemId, pageSize, offset).QueryRows(&books) return } } ================================================ FILE: models/LabelModel.go ================================================ package models import ( "strings" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) type Label struct { LabelId int `orm:"column(label_id);pk;auto;unique;description(项目标签id)" json:"label_id"` LabelName string `orm:"column(label_name);size(50);unique;description(项目标签名称)" json:"label_name"` BookNumber int `orm:"column(book_number);description(包涵项目数量)" json:"book_number"` } // TableName 获取对应数据库表名. func (m *Label) TableName() string { return "label" } // TableEngine 获取数据使用的引擎. func (m *Label) TableEngine() string { return "INNODB" } func (m *Label) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewLabel() *Label { return &Label{} } func (m *Label) FindFirst(field string, value interface{}) (*Label, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter(field, value).One(m) return m, err } //插入或更新标签. func (m *Label) InsertOrUpdate(labelName string) error { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("label_name", labelName).One(m) if err != nil && err != orm.ErrNoRows { return err } count, _ := o.QueryTable(NewBook().TableNameWithPrefix()).Filter("label__icontains", labelName).Count() m.BookNumber = int(count) m.LabelName = labelName if err == orm.ErrNoRows { err = nil m.LabelName = labelName _, err = o.Insert(m) } else { _, err = o.Update(m) } return err } //批量插入或更新标签. func (m *Label) InsertOrUpdateMulti(labels string) { if labels != "" { labelArray := strings.Split(labels, ",") for _, label := range labelArray { if label != "" { NewLabel().InsertOrUpdate(label) } } } } //删除标签 func (m *Label) Delete() error { o := orm.NewOrm() _, err := o.Raw("DELETE FROM "+m.TableNameWithPrefix()+" WHERE label_id= ?", m.LabelId).Exec() if err != nil { return err } return nil } //分页查找标签. func (m *Label) FindToPager(pageIndex, pageSize int) (labels []*Label, totalCount int, err error) { o := orm.NewOrm() count, err := o.QueryTable(m.TableNameWithPrefix()).Count() if err != nil { return } totalCount = int(count) offset := (pageIndex - 1) * pageSize _, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-book_number").Offset(offset).Limit(pageSize).All(&labels) if err == orm.ErrNoRows { logs.Info("没有查询到标签 ->", err) err = nil return } return } ================================================ FILE: models/Logs.go ================================================ package models import ( "errors" "sync/atomic" "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) var loggerQueue = &logQueue{channel: make(chan *Logger, 100), isRuning: 0} type logQueue struct { channel chan *Logger isRuning int32 } // Logger struct . type Logger struct { LoggerId int64 `orm:"pk;auto;unique;column(log_id)" json:"log_id"` MemberId int `orm:"column(member_id);type(int)" json:"member_id"` // 日志类别:operate 操作日志/ system 系统日志/ exception 异常日志 / document 文档操作日志 Category string `orm:"column(category);size(255);default(operate)" json:"category"` Content string `orm:"column(content);type(text)" json:"content"` OriginalData string `orm:"column(original_data);type(text)" json:"original_data"` PresentData string `orm:"column(present_data);type(text)" json:"present_data"` CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` UserAgent string `orm:"column(user_agent);size(500)" json:"user_agent"` IPAddress string `orm:"column(ip_address);size(255)" json:"ip_address"` } // TableName 获取对应数据库表名. func (m *Logger) TableName() string { return "logs" } // TableEngine 获取数据使用的引擎. func (m *Logger) TableEngine() string { return "INNODB" } func (m *Logger) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewLogger() *Logger { return &Logger{} } func (m *Logger) Add() error { if m.MemberId <= 0 { return errors.New("用户ID不能为空") } if m.Category == "" { m.Category = "system" } if m.Content == "" { return errors.New("日志内容不能为空") } loggerQueue.channel <- m if atomic.LoadInt32(&(loggerQueue.isRuning)) <= 0 { atomic.AddInt32(&(loggerQueue.isRuning), 1) go addLoggerAsync() } return nil } func addLoggerAsync() { defer atomic.AddInt32(&(loggerQueue.isRuning), -1) o := orm.NewOrm() for { logger := <-loggerQueue.channel o.Insert(logger) } } ================================================ FILE: models/Member.go ================================================ // Package models . package models import ( "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" "regexp" "strconv" "strings" "time" "github.com/go-ldap/ldap/v3" "math" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/utils" ) var LdapDefaultTimeout = 8 * time.Second type Member struct { MemberId int `orm:"pk;auto;unique;column(member_id)" json:"member_id"` Account string `orm:"size(100);unique;column(account);description(登录名)" json:"account"` RealName string `orm:"size(255);column(real_name);description(真实姓名)" json:"real_name"` Password string `orm:"size(1000);column(password);description(密码)" json:"-"` //认证方式: local 本地数据库 /ldap LDAP AuthMethod string `orm:"column(auth_method);default(local);size(50);description(授权方式 local:本地校验 ldap:LDAP用户校验)" json:"auth_method"` Description string `orm:"column(description);size(2000);description(描述)" json:"description"` Email string `orm:"size(100);column(email);unique;description(邮箱)" json:"email"` Phone string `orm:"size(255);column(phone);null;default(null);description(手机)" json:"phone"` Avatar string `orm:"size(1000);column(avatar);description(头像)" json:"avatar"` //用户角色:0 超级管理员 /1 管理员/ 2 普通用户/ 3 只读用户 . Role conf.SystemRole `orm:"column(role);type(int);default(1);index;description(用户角色: 0:超级管理员 1:管理员 2:普通用户 3:只读用户)" json:"role"` RoleName string `orm:"-" json:"role_name"` Status int `orm:"column(status);type(int);default(0);description(状态 0:启用 1:禁用)" json:"status"` //用户状态:0 正常/1 禁用 CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add;description(创建时间)" json:"create_time"` CreateAt int `orm:"type(int);column(create_at);description(创建人id)" json:"create_at"` LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null;description(最后登录时间)" json:"last_login_time"` //i18n Lang string `orm:"-"` } // TableName 获取对应数据库表名. func (m *Member) TableName() string { return "members" } // TableEngine 获取数据使用的引擎. func (m *Member) TableEngine() string { return "INNODB" } func (m *Member) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewMember() *Member { return &Member{} } // Login 用户登录. func (m *Member) Login(account string, password string) (*Member, error) { o := orm.NewOrm() member := &Member{} //err := o.QueryTable(m.TableNameWithPrefix()).Filter("account", account).Filter("status", 0).One(member) err := o.Raw("select * from md_members where (account = ? or email = ?) and status = 0 limit 1;", account, account).QueryRow(member) if err != nil { if web.AppConfig.DefaultBool("ldap_enable", false) { logs.Info("转入LDAP登陆 ->", account) return member.ldapLogin(account, password) } else if url, err := web.AppConfig.String("http_login_url"); url != "" { logs.Info("转入 HTTP 接口登陆 ->", account) return member.httpLogin(account, password) } else { logs.Error("user login for `%s`: %s", account, err) return member, ErrMemberNoExist } } switch member.AuthMethod { case "local": ok, err := utils.PasswordVerify(member.Password, password) if ok && err == nil { m.ResolveRoleName() return member, nil } case "ldap": return member.ldapLogin(account, password) case "http": return member.httpLogin(account, password) default: return member, ErrMemberAuthMethodInvalid } return member, ErrorMemberPasswordError } // TmpLogin 用于钉钉临时登录 //func (m *Member) TmpLogin(account string) (*Member, error) { // o := orm.NewOrm() // member := &Member{} // err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member) // if err != nil { // return member, ErrorMemberPasswordError // } // return member, nil //} // ldapLogin 通过LDAP登陆 func (m *Member) ldapLogin(account string, password string) (*Member, error) { if !web.AppConfig.DefaultBool("ldap_enable", false) { return m, ErrMemberAuthMethodInvalid } var err error var ldapOpt ldap.DialOpt ldap_scheme := web.AppConfig.DefaultString("ldap_scheme", "ldap") dialer := net.Dialer{Timeout: LdapDefaultTimeout} if ldap_scheme == "ldaps" { ldapOpt = ldap.DialWithTLSDialer(&tls.Config{InsecureSkipVerify: true}, &dialer) } else { ldapOpt = ldap.DialWithDialer(&dialer) } ldap_host, _ := web.AppConfig.String("ldap_host") ldap_port := web.AppConfig.DefaultInt("ldap_port", 3268) ldap_url := fmt.Sprintf("%s://%s:%d", ldap_scheme, ldap_host, ldap_port) lc, err := ldap.DialURL(ldap_url, ldapOpt) if err != nil { logs.Error("绑定 LDAP 用户失败 ->", err) return m, ErrLDAPConnect } defer lc.Close() ldapuser, _ := web.AppConfig.String("ldap_user") ldappass, _ := web.AppConfig.String("ldap_password") err = lc.Bind(ldapuser, ldappass) if err != nil { logs.Error("绑定 LDAP 用户失败 ->", err) return m, ErrLDAPFirstBind } ldapbase, _ := web.AppConfig.String("ldap_base") ldapfilter, _ := web.AppConfig.String("ldap_filter") ldapaccount, _ := web.AppConfig.String("ldap_account") ldapmail, _ := web.AppConfig.String("ldap_mail") // 判断account是否是email isEmail := false var email string ldapattr := ldapaccount if ok, err := regexp.MatchString(conf.RegexpEmail, account); ok && err == nil { isEmail = true email = account ldapattr = ldapmail } searchRequest := ldap.NewSearchRequest( ldapbase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, // 修改objectClass通过配置文件获取值 fmt.Sprintf("(&(%s)(%s=%s))", ldapfilter, ldapattr, account), []string{"dn", "mail", "cn", "ou", "sAMAccountName"}, nil, ) searchResult, err := lc.Search(searchRequest) if err != nil { logs.Error("绑定 LDAP 用户失败 ->", err) return m, ErrLDAPSearch } if len(searchResult.Entries) != 1 { return m, ErrLDAPUserNotFoundOrTooMany } userdn := searchResult.Entries[0].DN err = lc.Bind(userdn, password) if err != nil { logs.Error("绑定 LDAP 用户失败 ->", err) return m, ErrorMemberPasswordError } ldap_cn := searchResult.Entries[0].GetAttributeValue("cn") ldap_mail := searchResult.Entries[0].GetAttributeValue(ldapmail) // "mail" ldap_account := searchResult.Entries[0].GetAttributeValue(ldapaccount) // "sAMAccountName" m.RealName = ldap_cn m.Account = ldap_account m.AuthMethod = "ldap" // 如果ldap配置了email if len(ldap_mail) > 0 && strings.Contains(ldap_mail, "@") { // 如果member已配置email if len(m.Email) > 0 { // 如果member配置的email和ldap配置的email不同 if m.Email != ldap_mail { return m, fmt.Errorf("ldap配置的email(%s)与数据库中已有email({%s})不同, 请联系管理员修改", ldap_mail, m.Email) } } else { // 如果member未配置email,则用ldap的email配置 m.Email = ldap_mail } } else { // 如果ldap未配置email,则直接绑定到member if isEmail { m.Email = email } } if m.MemberId <= 0 { m.Avatar = "/static/images/headimgurl.jpg" m.Role = conf.SystemRole(web.AppConfig.DefaultInt("ldap_user_role", 2)) m.CreateTime = time.Now() err = m.Add() if err != nil { logs.Error("自动注册LDAP用户错误", err) return m, ErrorMemberPasswordError } m.ResolveRoleName() } else { // 更新ldap信息 err = m.Update("account", "real_name", "email", "auth_method") if err != nil { logs.Error("LDAP更新用户信息失败", err) return m, errors.New("LDAP更新用户信息失败") } m.ResolveRoleName() } return m, nil } func (m *Member) httpLogin(account, password string) (*Member, error) { urlStr, _ := web.AppConfig.String("http_login_url") if urlStr == "" { return nil, ErrMemberAuthMethodInvalid } val := url.Values{ "account": []string{account}, "password": []string{password}, "time": []string{strconv.FormatInt(time.Now().Unix(), 10)}, } h := md5.New() h.Write([]byte(val.Encode() + web.AppConfig.DefaultString("http_login_secret", ""))) val.Add("sn", hex.EncodeToString(h.Sum(nil))) resp, err := http.PostForm(urlStr, val) if err != nil { logs.Error("通过接口登录失败 -> ", urlStr, account, err) return nil, ErrHTTPServerFail } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { logs.Error("读取接口返回值失败 -> ", urlStr, account, err) return nil, ErrHTTPServerFail } logs.Info("HTTP 登录接口返回数据 ->", string(body)) var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { logs.Error("解析接口返回值失败 -> ", urlStr, account, string(body)) return nil, ErrHTTPServerFail } if code, ok := result["errcode"]; !ok || code.(float64) != 200 { if msg, ok := result["message"]; ok { return nil, errors.New(msg.(string)) } return nil, ErrHTTPServerFail } if m.MemberId <= 0 { member := NewMember() if email, ok := result["email"]; !ok || email == "" { return nil, errors.New("接口返回的数据缺少邮箱字段") } else { member.Email = email.(string) } if avatar, ok := result["avater"]; ok && avatar != "" { member.Avatar = avatar.(string) } else { member.Avatar = conf.URLForWithCdnImage("/static/images/headimgurl.jpg") } if realName, ok := result["real_name"]; ok && realName != "" { member.RealName = realName.(string) } member.Account = account member.Password = password member.AuthMethod = "http" member.Role = conf.SystemRole(web.AppConfig.DefaultInt("ldap_user_role", 2)) member.CreateTime = time.Now() if err := member.Add(); err != nil { logs.Error("自动注册用户错误", err) return m, ErrorMemberPasswordError } member.ResolveRoleName() *m = *member } return m, nil } // Add 添加一个用户. func (m *Member) Add() error { o := orm.NewOrm() if ok, err := regexp.MatchString(conf.RegexpAccount, m.Account); m.Account == "" || !ok || err != nil { return errors.New("账号只能由英文字母数字组成,且在3-50个字符") } if m.Email == "" { return errors.New("邮箱不能为空") } if ok, err := regexp.MatchString(conf.RegexpEmail, m.Email); !ok || err != nil || m.Email == "" { return errors.New("邮箱格式不正确") } if m.AuthMethod == "local" { if l := strings.Count(m.Password, ""); l < 6 || l >= 50 { return errors.New("密码不能为空且必须在6-50个字符之间") } } if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("email", m.Email).Count(); err == nil && c > 0 { return errors.New("邮箱已被使用") } hash, err := utils.PasswordHash(m.Password) if err != nil { logs.Error("加密用户密码失败 =>", err) return errors.New("加密用户密码失败") } m.Password = hash if m.AuthMethod == "" { m.AuthMethod = "local" } _, err = o.Insert(m) if err != nil { logs.Error("保存用户数据到数据时失败 =>", err) return errors.New("保存用户失败") } m.ResolveRoleName() return nil } // Update 更新用户信息. func (m *Member) Update(cols ...string) error { o := orm.NewOrm() if m.Email == "" { return errors.New("邮箱不能为空") } if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("email", m.Email).Exclude("member_id", m.MemberId).Count(); err == nil && c > 0 { return errors.New("邮箱已被使用") } if _, err := o.Update(m, cols...); err != nil { logs.Error("保存用户信息失败=>", err) return errors.New("保存用户信息失败") } return nil } func (m *Member) Find(id int, cols ...string) (*Member, error) { o := orm.NewOrm() if err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", id).One(m, cols...); err != nil { return m, err } m.ResolveRoleName() return m, nil } func (m *Member) ResolveRoleName() { if m.Role == conf.MemberSuperRole { m.RoleName = i18n.Tr(m.Lang, "uc.super_admin") } else if m.Role == conf.MemberAdminRole { m.RoleName = i18n.Tr(m.Lang, "uc.admin") } else if m.Role == conf.MemberGeneralRole { m.RoleName = i18n.Tr(m.Lang, "uc.user") } else if m.Role == conf.MemberReaderRole { m.RoleName = i18n.Tr(m.Lang, "uc.read_usr") } } // 根据账号查找用户. func (m *Member) FindByAccount(account string) (*Member, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("account", account).One(m) if err == nil { m.ResolveRoleName() } return m, err } // 批量查询用户 func (m *Member) FindByAccountList(accounts ...string) ([]*Member, error) { o := orm.NewOrm() var members []*Member _, err := o.QueryTable(m.TableNameWithPrefix()).Filter("account__in", accounts).All(&members) if err == nil { for _, item := range members { item.ResolveRoleName() } } return members, err } // 分页查找用户. func (m *Member) FindToPager(pageIndex, pageSize int) ([]*Member, int, error) { o := orm.NewOrm() var members []*Member offset := (pageIndex - 1) * pageSize totalCount, err := o.QueryTable(m.TableNameWithPrefix()).Count() if err != nil { return members, 0, err } _, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-member_id").Offset(offset).Limit(pageSize).All(&members) if err != nil { return members, 0, err } for _, tm := range members { tm.Lang = m.Lang tm.ResolveRoleName() } return members, int(totalCount), nil } func (m *Member) IsAdministrator() bool { if m == nil || m.MemberId <= 0 { return false } return m.Role == 0 || m.Role == 1 } // 根据指定字段查找用户. func (m *Member) FindByFieldFirst(field string, value interface{}) (*Member, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter(field, value).OrderBy("-member_id").One(m) return m, err } // 校验用户. func (m *Member) Valid(is_hash_password bool) error { //邮箱不能为空 if m.Email == "" { return ErrMemberEmailEmpty } //用户描述必须小于500字 if strings.Count(m.Description, "") > 500 { return ErrMemberDescriptionTooLong } if m.Role != conf.MemberGeneralRole && m.Role != conf.MemberSuperRole && m.Role != conf.MemberAdminRole && m.Role != conf.MemberReaderRole { return ErrMemberRoleError } if m.Status != 0 && m.Status != 1 { m.Status = 0 } //邮箱格式校验 if ok, err := regexp.MatchString(conf.RegexpEmail, m.Email); !ok || err != nil || m.Email == "" { return ErrMemberEmailFormatError } //如果是未加密密码,需要校验密码格式 if !is_hash_password { if l := strings.Count(m.Password, ""); m.Password == "" || l > 50 || l < 6 { return ErrMemberPasswordFormatError } } //校验邮箱是否呗使用 if member, err := NewMember().FindByFieldFirst("email", m.Account); err == nil && member.MemberId > 0 { if m.MemberId > 0 && m.MemberId != member.MemberId { return ErrMemberEmailExist } if m.MemberId <= 0 { return ErrMemberEmailExist } } if m.MemberId > 0 { //校验用户是否存在 if _, err := NewMember().Find(m.MemberId); err != nil { return err } } else { //校验账号格式是否正确 if ok, err := regexp.MatchString(conf.RegexpAccount, m.Account); m.Account == "" || !ok || err != nil { return ErrMemberAccountFormatError } //校验账号是否被使用 if member, err := NewMember().FindByAccount(m.Account); err == nil && member.MemberId > 0 { return ErrMemberExist } } return nil } // 删除一个用户. func (m *Member) Delete(oldId int, newId int) error { ormer := orm.NewOrm() o, err := ormer.Begin() if err != nil { return err } _, err = o.Raw("DELETE FROM md_dingtalk_accounts WHERE member_id = ?", oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("DELETE FROM md_workweixin_accounts WHERE member_id = ?", oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("DELETE FROM md_members WHERE member_id = ?", oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_attachment SET create_at = ? WHERE create_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_books SET member_id = ? WHERE member_id = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_document_history SET member_id=? WHERE member_id = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_document_history SET modify_at=? WHERE modify_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_documents SET member_id = ? WHERE member_id = ?;", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_documents SET modify_at = ? WHERE modify_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_blogs SET member_id = ? WHERE member_id = ?;", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_blogs SET modify_at = ? WHERE modify_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_templates SET modify_at = ? WHERE modify_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.Raw("UPDATE md_templates SET member_id = ? WHERE member_id = ?", newId, oldId).Exec() if err != nil { o.Rollback() return err } _, err = o.QueryTable(NewTeamMember()).Filter("member_id", oldId).Delete() if err != nil { o.Rollback() return err } //_,err = o.Raw("UPDATE md_relationship SET member_id = ? WHERE member_id = ?",newId,oldId).Exec() //if err != nil { // // if err != nil { // o.Rollback() // return err // } //} var relationshipList []*Relationship _, err = o.QueryTable(NewRelationship().TableNameWithPrefix()).Filter("member_id", oldId).Limit(math.MaxInt32).All(&relationshipList) if err == nil { for _, relationship := range relationshipList { //如果存在创始人,则删除 if relationship.RoleId == 0 { rel := NewRelationship() err = o.QueryTable(relationship.TableNameWithPrefix()).Filter("book_id", relationship.BookId).Filter("member_id", newId).One(rel) if err == nil { if _, err := o.Delete(relationship); err != nil { logs.Error(err) } relationship.RelationshipId = rel.RelationshipId } relationship.MemberId = newId relationship.RoleId = 0 if _, err := o.Update(relationship); err != nil { logs.Error(err) } } else { if _, err := o.Delete(relationship); err != nil { logs.Error(err) } } } } if err = o.Commit(); err != nil { o.Rollback() return err } return nil } ================================================ FILE: models/MemberResult.go ================================================ package models import ( "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" ) type MemberRelationshipResult struct { MemberId int `json:"member_id"` Account string `json:"account"` RealName string `json:"real_name"` Description string `json:"description"` Email string `json:"email"` Phone string `json:"phone"` Avatar string `json:"avatar"` Role conf.SystemRole `json:"role"` //用户角色:0 管理员/ 1 普通用户 Status int `json:"status"` //用户状态:0 正常/1 禁用 CreateTime time.Time `json:"create_time"` CreateAt int `json:"create_at"` RelationshipId int `json:"relationship_id"` BookId int `json:"book_id"` // RoleId 角色:0 创始人(创始人不能被移除) / 1 管理员/2 编辑者/3 观察者 RoleId conf.BookRole `json:"role_id"` RoleName string `json:"role_name"` } type SelectMemberResult struct { Result []KeyValueItem `json:"results"` } type KeyValueItem struct { Id int `json:"id"` Text string `json:"text"` } func NewMemberRelationshipResult() *MemberRelationshipResult { return &MemberRelationshipResult{} } func (m *MemberRelationshipResult) FromMember(member *Member) *MemberRelationshipResult { m.MemberId = member.MemberId m.Account = member.Account m.Description = member.Description m.Email = member.Email m.Phone = member.Phone m.Avatar = member.Avatar m.Role = member.Role m.Status = member.Status m.CreateTime = member.CreateTime m.CreateAt = member.CreateAt m.RealName = member.RealName return m } func (m *MemberRelationshipResult) ResolveRoleName(lang string) *MemberRelationshipResult { if m.RoleId == conf.BookAdmin { m.RoleName = i18n.Tr(lang, "common.administrator") } else if m.RoleId == conf.BookEditor { m.RoleName = i18n.Tr(lang, "common.editor") } else if m.RoleId == conf.BookObserver { m.RoleName = i18n.Tr(lang, "common.observer") } return m } // 根据项目ID查询用户 func (m *MemberRelationshipResult) FindForUsersByBookId(lang string, bookId, pageIndex, pageSize int) ([]*MemberRelationshipResult, int, error) { o := orm.NewOrm() var members []*MemberRelationshipResult sql1 := "SELECT * FROM md_relationship AS rel LEFT JOIN md_members as mdmb ON rel.member_id = mdmb.member_id WHERE rel.book_id = ? ORDER BY rel.relationship_id DESC limit ? offset ?" sql2 := "SELECT count(*) AS total_count FROM md_relationship AS rel LEFT JOIN md_members as mdmb ON rel.member_id = mdmb.member_id WHERE rel.book_id = ?" var total_count int err := o.Raw(sql2, bookId).QueryRow(&total_count) if err != nil { return members, 0, err } offset := (pageIndex - 1) * pageSize _, err = o.Raw(sql1, bookId, pageSize, offset).QueryRows(&members) if err != nil { return members, 0, err } for _, item := range members { item.ResolveRoleName(lang) } return members, total_count, nil } // 查询指定文档中不存在的用户列表 func (m *MemberRelationshipResult) FindNotJoinUsersByAccount(bookId, limit int, account string) ([]*Member, error) { o := orm.NewOrm() sql := "SELECT m.* FROM md_members as m LEFT JOIN md_relationship as rel ON m.member_id=rel.member_id AND rel.book_id = ? WHERE rel.relationship_id IS NULL AND m.account LIKE ? LIMIT 0,?;" var members []*Member _, err := o.Raw(sql, bookId, account, limit).QueryRows(&members) return members, err } // 根据姓名以及用户名模糊查询指定文档中不存在的用户列表 func (m *MemberRelationshipResult) FindNotJoinUsersByAccountOrRealName(bookId, limit int, keyWord string) ([]*Member, error) { o := orm.NewOrm() sql := "SELECT m.* FROM md_members as m LEFT JOIN md_relationship as rel ON rel.member_id = m.member_id AND rel.book_id = ? WHERE rel.relationship_id IS NULL AND (m.real_name LIKE ? OR m.account LIKE ?) LIMIT ? OFFSET 0;" var members []*Member _, err := o.Raw(sql, bookId, keyWord, keyWord, limit).QueryRows(&members) return members, err } ================================================ FILE: models/MemberToken.go ================================================ package models import ( "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) type MemberToken struct { TokenId int `orm:"column(token_id);pk;auto;unique" json:"token_id"` MemberId int `orm:"column(member_id);type(int)" json:"member_id"` Token string `orm:"column(token);size(150);index" json:"token"` Email string `orm:"column(email);size(255)" json:"email"` IsValid bool `orm:"column(is_valid)" json:"is_valid"` ValidTime time.Time `orm:"column(valid_time);null" json:"valid_time"` SendTime time.Time `orm:"column(send_time);auto_now_add;type(datetime)" json:"send_time"` } // TableName 获取对应数据库表名. func (m *MemberToken) TableName() string { return "member_token" } // TableEngine 获取数据使用的引擎. func (m *MemberToken) TableEngine() string { return "INNODB" } func (m *MemberToken) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewMemberToken() *MemberToken { return &MemberToken{} } func (m *MemberToken) InsertOrUpdate() (*MemberToken, error) { o := orm.NewOrm() if m.TokenId > 0 { _, err := o.Update(m) return m, err } _, err := o.Insert(m) return m, err } func (m *MemberToken) FindByFieldFirst(field string, value interface{}) (*MemberToken, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter(field, value).OrderBy("-token_id").One(m) return m, err } func (m *MemberToken) FindSendCount(mail string, start_time time.Time, end_time time.Time) (int, error) { o := orm.NewOrm() c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("send_time__gte", start_time.Format("2006-01-02 15:04:05")).Filter("send_time__lte", end_time.Format("2006-01-02 15:04:05")).Count() if err != nil { return 0, err } return int(c), nil } ================================================ FILE: models/Migrations.go ================================================ package models import ( "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) type Migration struct { MigrationId int `orm:"column(migration_id);pk;auto;unique;" json:"migration_id"` Name string `orm:"column(name);size(500)" json:"name"` Statements string `orm:"column(statements);type(text);null" json:"statements"` Status string `orm:"column(status);default(update)" json:"status"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` Version int64 `orm:"type(bigint);column(version);unique" json:"version"` } // TableName 获取对应数据库表名. func (m *Migration) TableName() string { return "migrations" } // TableEngine 获取数据使用的引擎. func (m *Migration) TableEngine() string { return "INNODB" } func (m *Migration) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewMigration() *Migration { return &Migration{} } func (m *Migration) FindFirst() (*Migration, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).OrderBy("-migration_id").One(m) return m, err } ================================================ FILE: models/Options.go ================================================ package models import ( "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) // Option struct . type Option struct { OptionId int `orm:"column(option_id);pk;auto;unique;" json:"option_id"` OptionTitle string `orm:"column(option_title);size(500)" json:"option_title"` OptionName string `orm:"column(option_name);unique;size(80)" json:"option_name"` OptionValue string `orm:"column(option_value);type(text);null" json:"option_value"` Remark string `orm:"column(remark);type(text);null" json:"remark"` } // TableName 获取对应数据库表名. func (m *Option) TableName() string { return "options" } // TableEngine 获取数据使用的引擎. func (m *Option) TableEngine() string { return "INNODB" } func (m *Option) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewOption() *Option { return &Option{} } func (p *Option) Find(id int) (*Option, error) { o := orm.NewOrm() p.OptionId = id if err := o.Read(p); err != nil { return p, err } return p, nil } func (p *Option) FindByKey(key string) (*Option, error) { o := orm.NewOrm() p.OptionName = key if err := o.Read(p); err != nil { return p, err } return p, nil } func GetOptionValue(key, def string) string { if option, err := NewOption().FindByKey(key); err == nil { return option.OptionValue } return def } func (p *Option) InsertOrUpdate() error { o := orm.NewOrm() var err error if p.OptionId > 0 || o.QueryTable(p.TableNameWithPrefix()).Filter("option_name", p.OptionName).Exist() { _, err = o.Update(p) } else { _, err = o.Insert(p) } return err } func (p *Option) InsertMulti(option ...Option) error { o := orm.NewOrm() _, err := o.InsertMulti(len(option), option) return err } func (p *Option) All() ([]*Option, error) { o := orm.NewOrm() var options []*Option _, err := o.QueryTable(p.TableNameWithPrefix()).All(&options) if err != nil { return options, err } return options, nil } func (m *Option) Init() error { o := orm.NewOrm() if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "ENABLED_REGISTER").Exist() { option := NewOption() option.OptionValue = "false" option.OptionName = "ENABLED_REGISTER" option.OptionTitle = "是否启用注册" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "ENABLE_DOCUMENT_HISTORY").Exist() { option := NewOption() option.OptionValue = "true" option.OptionName = "ENABLE_DOCUMENT_HISTORY" option.OptionTitle = "是否启用文档历史" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "ENABLED_CAPTCHA").Exist() { option := NewOption() option.OptionValue = "true" option.OptionName = "ENABLED_CAPTCHA" option.OptionTitle = "是否启用验证码" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "ENABLE_ANONYMOUS").Exist() { option := NewOption() option.OptionValue = "false" option.OptionName = "ENABLE_ANONYMOUS" option.OptionTitle = "启用匿名访问" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "SITE_NAME").Exist() { option := NewOption() option.OptionValue = "MinDoc文档管理系统" option.OptionName = "SITE_NAME" option.OptionTitle = "站点名称" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "site_description").Exist() { option := NewOption() option.OptionValue = "MinDoc 是一款针对IT团队开发的简单好用的文档管理系统,可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,支持Markdown和富文本两种编辑器,能够满足大部分中小团队的文档管理需求。" option.OptionName = "site_description" option.OptionTitle = "站点描述" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "site_beian").Exist() { option := NewOption() option.OptionValue = "" option.OptionName = "site_beian" option.OptionTitle = "域名备案" if _, err := o.Insert(option); err != nil { return err } } if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "language").Exist() { option := NewOption() option.OptionValue = "zh-cn" option.OptionName = "language" option.OptionTitle = "站点语言" if _, err := o.Insert(option); err != nil { return err } } return nil } func (m *Option) Update() error { o := orm.NewOrm() if !o.QueryTable(m.TableNameWithPrefix()).Filter("option_name", "language").Exist() { option := NewOption() option.OptionValue = "zh-cn" option.OptionName = "language" option.OptionTitle = "站点语言" if _, err := o.Insert(option); err != nil { return err } } return nil } ================================================ FILE: models/Relationship.go ================================================ package models import ( "errors" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) type Relationship struct { RelationshipId int `orm:"pk;auto;unique;column(relationship_id)" json:"relationship_id"` MemberId int `orm:"column(member_id);type(int);description(作者id)" json:"member_id"` BookId int `orm:"column(book_id);type(int);description(所属项目id)" json:"book_id"` // RoleId 角色:0 创始人(创始人不能被移除) / 1 管理员/2 编辑者/3 观察者 RoleId conf.BookRole `orm:"column(role_id);type(int);description(角色-配置文件里写死:0 创始人-不能被移除 / 1 管理员/2 编辑者/3 观察者)" json:"role_id"` } // TableName 获取对应数据库表名. 用户和项目的关联表 func (m *Relationship) TableName() string { return "relationship" } func (m *Relationship) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } // TableEngine 获取数据使用的引擎. func (m *Relationship) TableEngine() string { return "INNODB" } // 联合唯一键 func (m *Relationship) TableUnique() [][]string { return [][]string{ {"member_id", "book_id"}, } } func (m *Relationship) QueryTable() orm.QuerySeter { return orm.NewOrm().QueryTable(m.TableNameWithPrefix()) } func NewRelationship() *Relationship { return &Relationship{} } func (m *Relationship) Find(id int) (*Relationship, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("relationship_id", id).One(m) return m, err } //查询指定项目的创始人. func (m *Relationship) FindFounder(book_id int) (*Relationship, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("book_id", book_id).Filter("role_id", 0).One(m) return m, err } func (m *Relationship) UpdateRoleId(bookId, memberId int, roleId conf.BookRole) (*Relationship, error) { o := orm.NewOrm() book := NewBook() book.BookId = bookId if err := o.Read(book); err != nil { logs.Error("UpdateRoleId => ", err) return m, errors.New("项目不存在") } err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", memberId).Filter("book_id", bookId).One(m) if err == orm.ErrNoRows { m = NewRelationship() m.BookId = bookId m.MemberId = memberId m.RoleId = roleId } else if err != nil { return m, err } else if m.RoleId == conf.BookFounder { return m, errors.New("不能变更创始人的权限") } m.RoleId = roleId if m.RelationshipId > 0 { _, err = o.Update(m) } else { _, err = o.Insert(m) } return m, err } func (m *Relationship) FindForRoleId(bookId, memberId int) (conf.BookRole, error) { o := orm.NewOrm() relationship := NewRelationship() err := o.QueryTable(m.TableNameWithPrefix()).Filter("book_id", bookId).Filter("member_id", memberId).One(relationship) if err != nil { return conf.BookRoleNoSpecific, err } return relationship.RoleId, nil } func (m *Relationship) FindByBookIdAndMemberId(book_id, member_id int) (*Relationship, error) { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("book_id", book_id).Filter("member_id", member_id).One(m) return m, err } func (m *Relationship) Insert() error { o := orm.NewOrm() _, err := o.Insert(m) return err } func (m *Relationship) Update(txOrm orm.TxOrmer) error { _, err := txOrm.Update(m) if err != nil { txOrm.Rollback() } return err } func (m *Relationship) DeleteByBookIdAndMemberId(book_id, member_id int) error { o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("book_id", book_id).Filter("member_id", member_id).One(m) if err == orm.ErrNoRows { return errors.New("用户未参与该项目") } if m.RoleId == conf.BookFounder { return errors.New("不能删除创始人") } _, err = o.Delete(m) if err != nil { logs.Error("删除项目参与者 => ", err) return errors.New("删除失败") } return nil } func (m *Relationship) Transfer(book_id, founder_id, receive_id int) error { ormer := orm.NewOrm() founder := NewRelationship() err := ormer.QueryTable(m.TableNameWithPrefix()).Filter("book_id", book_id).Filter("member_id", founder_id).One(founder) if err != nil { return err } if founder.RoleId != conf.BookFounder { return errors.New("转让者不是创始人") } receive := NewRelationship() err = ormer.QueryTable(m.TableNameWithPrefix()).Filter("book_id", book_id).Filter("member_id", receive_id).One(receive) if err != orm.ErrNoRows && err != nil { return err } o, _ := ormer.Begin() founder.RoleId = conf.BookAdmin receive.MemberId = receive_id receive.RoleId = conf.BookFounder receive.BookId = book_id if err := founder.Update(o); err != nil { return err } if receive.RelationshipId > 0 { if _, err := o.Update(receive); err != nil { o.Rollback() return err } } else { if _, err := o.Insert(receive); err != nil { o.Rollback() return err } } return o.Commit() } ================================================ FILE: models/Team.go ================================================ package models import ( "errors" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) //团队. type Team struct { TeamId int `orm:"column(team_id);pk;auto;unique;" json:"team_id"` TeamName string `orm:"column(team_name);size(255);description(团队名称)" json:"team_name"` MemberId int `orm:"column(member_id);type(int);description(创建人id)" json:"member_id"` IsDelete bool `orm:"column(is_delete);default(false);description(是否删除 false:否 true:是)" json:"is_delete"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add;description(创建时间)" json:"create_time"` MemberCount int `orm:"-" json:"member_count"` BookCount int `orm:"-" json:"book_count"` MemberName string `orm:"-" json:"member_name"` } // TableName 获取对应数据库表名. func (t *Team) TableName() string { return "teams" } // TableEngine 获取数据使用的引擎. func (t *Team) TableEngine() string { return "INNODB" } func (t *Team) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + t.TableName() } func NewTeam() *Team { return &Team{} } // 查询一个团队. func (t *Team) First(id int, cols ...string) (*Team, error) { if id <= 0 { return nil, orm.ErrNoRows } o := orm.NewOrm() err := o.QueryTable(t.TableNameWithPrefix()).Filter("team_id", id).One(t, cols...) if err != nil { logs.Error("查询团队失败 ->", id, err) return nil, err } t.Include() return t, err } func (t *Team) Delete(id int) (err error) { if id <= 0 { return ErrInvalidParameter } ormer := orm.NewOrm() o, err := ormer.Begin() if err != nil { logs.Error("开启事物时出错 ->", err) return } _, err = o.QueryTable(t.TableNameWithPrefix()).Filter("team_id", id).Delete() if err != nil { logs.Error("删除团队时出错 ->", err) o.Rollback() return } _, err = o.Raw("delete from md_team_member where team_id=?;", id).Exec() if err != nil { logs.Error("删除团队成员时出错 ->", err) o.Rollback() return } _, err = o.Raw("delete from md_team_relationship where team_id=?;", id).Exec() if err != nil { logs.Error("删除团队项目时出错 ->", err) o.Rollback() return err } err = o.Commit() return } //分页查询团队. func (t *Team) FindToPager(pageIndex, pageSize int) (list []*Team, totalCount int, err error) { o := orm.NewOrm() offset := (pageIndex - 1) * pageSize _, err = o.QueryTable(t.TableNameWithPrefix()).OrderBy("-team_id").Offset(offset).Limit(pageSize).All(&list) if err != nil { return } c, err := o.QueryTable(t.TableNameWithPrefix()).Count() if err != nil { return } totalCount = int(c) for _, item := range list { item.Include() } return } func (t *Team) Include() { o := orm.NewOrm() if member, err := NewMember().Find(t.MemberId, "account", "real_name"); err == nil { if member.RealName != "" { t.MemberName = member.RealName } else { t.MemberName = member.Account } } if c, err := o.QueryTable(NewTeamRelationship().TableNameWithPrefix()).Filter("team_id", t.TeamId).Count(); err == nil { t.BookCount = int(c) } if c, err := o.QueryTable(NewTeamMember().TableNameWithPrefix()).Filter("team_id", t.TeamId).Count(); err == nil { t.MemberCount = int(c) } } //更新或添加一个团队. func (t *Team) Save(cols ...string) (err error) { if t.TeamName == "" { return NewError(5001, "团队名称不能为空") } o := orm.NewOrm() if t.TeamId <= 0 && o.QueryTable(t.TableNameWithPrefix()).Filter("team_name", t.TeamName).Exist() { return errors.New("团队名称已存在") } if t.TeamId <= 0 { _, err = o.Insert(t) } else { _, err = o.Update(t, cols...) } if err != nil { logs.Error("在保存团队时出错 ->", err) } return } ================================================ FILE: models/TeamMember.go ================================================ package models import ( "errors" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" ) type TeamMember struct { TeamMemberId int `orm:"column(team_member_id);pk;auto;unique;" json:"team_member_id"` TeamId int `orm:"column(team_id);type(int);description(团队id)" json:"team_id"` MemberId int `orm:"column(member_id);type(int);description(成员id)" json:"member_id"` // RoleId 角色:0 创始人(创始人不能被移除) / 1 管理员/2 编辑者/3 观察者 RoleId conf.BookRole `orm:"column(role_id);type(int);description(RoleId 角色:0 创始人-创始人不能被移除 / 1 管理员/2 编辑者/3 观察者)" json:"role_id"` RoleName string `orm:"-" json:"role_name"` Account string `orm:"-" json:"account"` RealName string `orm:"-" json:"real_name"` Avatar string `orm:"-" json:"avatar"` Lang string `orm:"-"` } // TableName 获取对应数据库表名. func (m *TeamMember) TableName() string { return "team_member" } func (m *TeamMember) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } // TableEngine 获取数据使用的引擎. func (m *TeamMember) TableEngine() string { return "INNODB" } // 联合唯一键 func (m *TeamMember) TableUnique() [][]string { return [][]string{{"team_id", "member_id"}} } func (m *TeamMember) QueryTable() orm.QuerySeter { return orm.NewOrm().QueryTable(m.TableNameWithPrefix()) } func NewTeamMember() *TeamMember { return &TeamMember{} } func (m *TeamMember) SetLang(lang string) *TeamMember { m.Lang = lang return m } func (m *TeamMember) First(id int, cols ...string) (*TeamMember, error) { if id <= 0 { return nil, errors.New("参数错误") } o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("team_member_id", id).One(m, cols...) if err != nil && err != orm.ErrNoRows { logs.Error("查询团队成员错误 ->", err) } return m.Include(), err } func (m *TeamMember) ChangeRoleId(teamId int, memberId int, roleId conf.BookRole) (member *TeamMember, err error) { if teamId <= 0 || memberId <= 0 || roleId <= 0 || roleId > conf.BookObserver { return nil, ErrInvalidParameter } o := orm.NewOrm() err = o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", teamId).Filter("member_id", memberId).OrderBy("-team_member_id").One(m) if err != nil { logs.Error("查询团队用户时失败 ->", err) return m, err } m.RoleId = roleId err = m.Save("role_id") if err == nil { m.Include() } return m, err } //查询团队中指定的用户. func (m *TeamMember) FindFirst(teamId, memberId int) (*TeamMember, error) { if teamId <= 0 || memberId <= 0 { return nil, ErrInvalidParameter } o := orm.NewOrm() err := o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", teamId).Filter("member_id", memberId).One(m) if err != nil { logs.Error("查询团队用户失败 ->", err) return nil, err } return m.Include(), nil } //更新或插入团队用户. func (m *TeamMember) Save(cols ...string) (err error) { if m.TeamId <= 0 { return errors.New("团队不能为空") } if m.MemberId <= 0 { return errors.New("用户不能为空") } o := orm.NewOrm() if !o.QueryTable(NewTeam().TableNameWithPrefix()).Filter("team_id", m.TeamId).Exist() { return errors.New("团队不存在") } if !o.QueryTable(NewMember()).Filter("member_id", m.MemberId).Filter("status", 0).Exist() { return errors.New("用户不存在或已禁用") } if m.TeamMemberId <= 0 { if o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", m.TeamId).Filter("member_id", m.MemberId).Exist() { return errors.New("团队中已存在该用户") } _, err = o.Insert(m) } else { _, err = o.Update(m, cols...) } if err != nil { logs.Error("在保存团队时出错 ->", err) } return } //删除一个团队用户. func (m *TeamMember) Delete(id int) (err error) { if id <= 0 { return ErrInvalidParameter } _, err = orm.NewOrm().QueryTable(m.TableNameWithPrefix()).Filter("team_member_id", id).Delete() if err != nil { logs.Error("删除团队用户时出错 ->", err) } return } //分页查询团队用户. func (m *TeamMember) FindToPager(teamId, pageIndex, pageSize int) (list []*TeamMember, totalCount int, err error) { if teamId <= 0 { err = ErrInvalidParameter return } offset := (pageIndex - 1) * pageSize o := orm.NewOrm() _, err = o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", teamId).Offset(offset).Limit(pageSize).All(&list) if err != nil { if err != orm.ErrNoRows { logs.Error("查询团队成员失败 ->", err) } return } c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", teamId).Count() if err != nil { return } totalCount = int(c) //将来优化 for _, item := range list { item.Lang = m.Lang item.Include() } return } //查询关联数据. func (m *TeamMember) Include() *TeamMember { if member, err := NewMember().Find(m.MemberId, "account", "real_name", "avatar"); err == nil { m.Account = member.Account m.RealName = member.RealName m.Avatar = member.Avatar } if m.RoleId == 0 { m.RoleName = i18n.Tr(m.Lang, "common.creator") //"创始人" } else if m.RoleId == 1 { m.RoleName = i18n.Tr(m.Lang, "common.administrator") //"管理员" } else if m.RoleId == 2 { m.RoleName = i18n.Tr(m.Lang, "common.editor") //"编辑者" } else if m.RoleId == 3 { m.RoleName = i18n.Tr(m.Lang, "common.observer") //"观察者" } return m } //查询未加入团队的用户。 func (m *TeamMember) FindNotJoinMemberByAccount(teamId int, account string, limit int) (*SelectMemberResult, error) { if teamId <= 0 { return nil, ErrInvalidParameter } o := orm.NewOrm() sql := `select mdmb.member_id,mdmb.account,mdmb.real_name,team.team_member_id from md_members as mdmb left join md_team_member as team on team.team_id = ? and mdmb.member_id = team.member_id where mdmb.account like ? or mdmb.real_name like ? AND team_member_id IS NULL order by mdmb.member_id desc limit ?;` members := make([]*Member, 0) _, err := o.Raw(sql, teamId, "%"+account+"%", "%"+account+"%", limit).QueryRows(&members) if err != nil { logs.Error("查询团队用户时出错 ->", err) return nil, err } result := SelectMemberResult{} items := make([]KeyValueItem, 0) for _, member := range members { item := KeyValueItem{} item.Id = member.MemberId item.Text = member.Account + "[" + member.RealName + "]" items = append(items, item) } result.Result = items return &result, err } func (m *TeamMember) FindByBookIdAndMemberId(bookId, memberId int) (*TeamMember, error) { if bookId <= 0 || memberId <= 0 { return nil, ErrInvalidParameter } //一个用户可能在多个团队中,且一个项目可能有多个团队参与。因此需要查询用户最大权限。 sql := `select * from md_team_member as team where team.team_id in (select rel.team_id from md_team_relationship as rel where rel.book_id = ?) and team.member_id = ? order by team.role_id asc limit 1;` o := orm.NewOrm() err := o.Raw(sql, bookId, memberId).QueryRow(m) if err != nil { logs.Error("查询用户项目所在团队失败 ->bookId=", bookId, " memberId=", memberId, err) return nil, err } return m, nil } ================================================ FILE: models/TeamRelationship.go ================================================ package models import ( "errors" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) type TeamRelationship struct { TeamRelationshipId int `orm:"column(team_relationship_id);pk;auto;unique;" json:"team_relationship_id"` BookId int `orm:"column(book_id);description(项目id)" json:"book_id"` TeamId int `orm:"column(team_id);description(团队id)" json:"team_id"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add;description(创建时间)" json:"create_time"` TeamName string `orm:"-" json:"team_name"` MemberCount int `orm:"-" json:"member_count"` BookMemberId int `orm:"-" json:"book_member_id"` BookMemberName string `orm:"-" json:"book_member_name"` BookName string `orm:"-" json:"book_name"` } // TableName 获取对应数据库表名. func (m *TeamRelationship) TableName() string { return "team_relationship" } func (m *TeamRelationship) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } // TableEngine 获取数据使用的引擎. func (m *TeamRelationship) TableEngine() string { return "INNODB" } // 联合唯一键 func (m *TeamRelationship) TableUnique() [][]string { return [][]string{{"team_id", "book_id"}} } func (m *TeamRelationship) QueryTable() orm.QuerySeter { return orm.NewOrm().QueryTable(m.TableNameWithPrefix()) } func NewTeamRelationship() *TeamRelationship { return &TeamRelationship{} } func (m *TeamRelationship) First(teamId int, cols ...string) (*TeamRelationship, error) { if teamId <= 0 { return nil, ErrInvalidParameter } err := m.QueryTable().Filter("team_id", teamId).One(m, cols...) if err != nil { logs.Error("查询项目团队失败 ->", err) } return m, err } //查找指定项目的指定团队. func (m *TeamRelationship) FindByBookId(bookId int, teamId int) (*TeamRelationship, error) { if teamId <= 0 || bookId <= 0 { return nil, ErrInvalidParameter } err := m.QueryTable().Filter("team_id", teamId).Filter("book_id", bookId).One(m) if err != nil { logs.Error("查询项目团队失败 ->", err) } return m, err } //删除指定项目的指定团队. func (m *TeamRelationship) DeleteByBookId(bookId int, teamId int) error { err := m.QueryTable().Filter("team_id", teamId).Filter("book_id", bookId).One(m) if err != nil { logs.Error("查询项目团队失败 ->", err) return err } m.Include() return m.Delete(m.TeamRelationshipId) } //保存团队项目. func (m *TeamRelationship) Save(cols ...string) (err error) { if m.TeamId <= 0 || m.BookId <= 0 { return ErrInvalidParameter } if (m.TeamRelationshipId > 0 && m.QueryTable().Filter("book_id", m.BookId).Filter("team_id", m.TeamId).Filter("team_relationship_id__ne", m.TeamRelationshipId).Exist()) || m.TeamRelationshipId <= 0 && m.QueryTable().Filter("book_id", m.BookId).Filter("team_id", m.TeamId).Exist() { return errors.New("当前团队已加入该项目") } if m.TeamRelationshipId > 0 { _, err = orm.NewOrm().Update(m) } else { _, err = orm.NewOrm().Insert(m) } if err != nil { logs.Error("保存团队项目时出错 ->", err) } return } func (m *TeamRelationship) Delete(teamRelId int) (err error) { if teamRelId <= 0 { return ErrInvalidParameter } _, err = m.QueryTable().Filter("team_relationship_id", teamRelId).Delete() if err != nil { logs.Error("删除团队项目失败 ->", err) } return } //分页查询团队项目. func (m *TeamRelationship) FindToPager(teamId, pageIndex, pageSize int) (list []*TeamRelationship, totalCount int, err error) { if teamId <= 0 { err = ErrInvalidParameter return } offset := (pageIndex - 1) * pageSize o := orm.NewOrm() _, err = o.QueryTable(m.TableNameWithPrefix()).Filter("team_id", teamId).OrderBy("-team_relationship_id").Offset(offset).Limit(pageSize).All(&list) if err != nil { logs.Error("查询团队项目时出错 ->", err) return } count, err := m.QueryTable().Filter("team_id", teamId).Count() if err != nil { logs.Error("查询团队项目时出错 ->", err) return } totalCount = int(count) for _, item := range list { item.Include() } return } //加载附加数据. func (m *TeamRelationship) Include() (*TeamRelationship, error) { if m.BookId > 0 { b, err := NewBook().Find(m.BookId, "book_name", "identify", "member_id") if err != nil { return m, err } m.BookName = b.BookName m.BookMemberId = b.MemberId if b.MemberId > 0 { member, err := NewMember().Find(b.MemberId, "account", "real_name") if err != nil { return m, err } if member.RealName == "" { m.BookMemberName = member.Account } else { m.BookMemberName = member.RealName } } } if m.TeamId > 0 { team, err := NewTeam().First(m.TeamId) if err == nil { m.TeamName = team.TeamName m.MemberCount = team.MemberCount } } return m, nil } //查询未加入团队的项目. func (m *TeamRelationship) FindNotJoinBookByName(teamId int, bookName string, limit int) (*SelectMemberResult, error) { if teamId <= 0 { return nil, ErrInvalidParameter } o := orm.NewOrm() sql := `select book.book_id,book.book_name from md_books as book where book.book_id not in (select team.book_id from md_team_relationship as team where team_id=?) and book.book_name like ? order by book_id desc limit ?;` books := make([]*Book, 0) _, err := o.Raw(sql, teamId, "%"+bookName+"%", limit).QueryRows(&books) if err != nil { logs.Error("查询团队项目时出错 ->", err) return nil, err } result := SelectMemberResult{} items := make([]KeyValueItem, 0) for _, book := range books { item := KeyValueItem{} item.Id = book.BookId item.Text = book.BookName items = append(items, item) } result.Result = items return &result, err } //查找指定项目中未加入的团队. func (m *TeamRelationship) FindNotJoinBookByBookIdentify(bookId int, teamName string, limit int) (*SelectMemberResult, error) { if bookId <= 0 || teamName == "" { return nil, ErrInvalidParameter } o := orm.NewOrm() sql := `select * from md_teams as team where team.team_id not in (select rel.team_id from md_team_relationship as rel where rel.book_id = ?) and team.team_name like ? order by team.team_id desc limit ?;` teams := make([]*Team, 0) _, err := o.Raw(sql, bookId, "%"+teamName+"%", limit).QueryRows(&teams) if err != nil { logs.Error("查询团队项目时出错 ->", err) return nil, err } result := SelectMemberResult{} items := make([]KeyValueItem, 0) for _, team := range teams { item := KeyValueItem{} item.Id = team.TeamId item.Text = team.TeamName items = append(items, item) } result.Result = items return &result, err } //查询指定项目的团队. func (m *TeamRelationship) FindByBookToPager(bookId, pageIndex, pageSize int) (list []*TeamRelationship, totalCount int, err error) { if bookId <= 0 { err = ErrInvalidParameter return } offset := (pageIndex - 1) * pageSize o := orm.NewOrm() _, err = o.QueryTable(m.TableNameWithPrefix()).Filter("book_id", bookId).OrderBy("-team_relationship_id").Offset(offset).Limit(pageSize).All(&list) if err != nil { logs.Error("查询团队项目时出错 ->", err) return } count, err := m.QueryTable().Filter("book_id", bookId).Count() if err != nil { logs.Error("查询团队项目时出错 ->", err) return } totalCount = int(count) for _, item := range list { item.Include() } return } ================================================ FILE: models/Template.go ================================================ package models import ( "errors" "time" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" ) type Template struct { TemplateId int `orm:"column(template_id);pk;auto;unique;" json:"template_id"` TemplateName string `orm:"column(template_name);size(500);" json:"template_name"` MemberId int `orm:"column(member_id);index" json:"member_id"` BookId int `orm:"column(book_id);index" json:"book_id"` BookName string `orm:"-" json:"book_name"` //是否是全局模板:0 否/1 是; 全局模板在所有项目中都可以使用;否则只能在创建模板的项目中使用 IsGlobal int `orm:"column(is_global);default(0)" json:"is_global"` TemplateContent string `orm:"column(template_content);type(text);null" json:"template_content"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` CreateName string `orm:"-" json:"create_name"` ModifyTime time.Time `orm:"column(modify_time);type(datetime);auto_now" json:"modify_time"` ModifyAt int `orm:"column(modify_at);type(int)" json:"-"` ModifyName string `orm:"-" json:"modify_name"` Version int64 `orm:"type(bigint);column(version)" json:"version"` } // TableName 获取对应数据库表名. func (m *Template) TableName() string { return "templates" } // TableEngine 获取数据使用的引擎. func (m *Template) TableEngine() string { return "INNODB" } func (m *Template) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func NewTemplate() *Template { return &Template{} } //查询指定ID的模板 func (t *Template) Find(templateId int) (*Template, error) { if templateId <= 0 { return t, ErrInvalidParameter } o := orm.NewOrm() err := o.QueryTable(t.TableNameWithPrefix()).Filter("template_id", templateId).One(t) if err != nil { logs.Error("查询模板时失败 ->%s", err) } return t, err } //查询属于指定项目的模板. func (t *Template) FindByBookId(bookId int) ([]*Template, error) { if bookId <= 0 { return nil, ErrInvalidParameter } o := orm.NewOrm() var templateList []*Template _, err := o.QueryTable(t.TableNameWithPrefix()).Filter("book_id", bookId).OrderBy("-template_id").All(&templateList) if err != nil { logs.Error("查询模板列表失败 ->", err) } return templateList, err } //查询指定项目所有可用模板列表. func (t *Template) FindAllByBookId(bookId int) ([]*Template, error) { if bookId <= 0 { return nil, ErrInvalidParameter } o := orm.NewOrm() cond := orm.NewCondition() cond1 := cond.And("book_id", bookId).Or("is_global", 1) qs := o.QueryTable(t.TableNameWithPrefix()) var templateList []*Template _, err := qs.SetCond(cond1).OrderBy("-template_id").All(&templateList) if err != nil { logs.Error("查询模板列表失败 ->", err) } return templateList, err } //删除一个模板 func (t *Template) Delete(templateId int, memberId int) error { if templateId <= 0 { return ErrInvalidParameter } o := orm.NewOrm() qs := o.QueryTable(t.TableNameWithPrefix()).Filter("template_id", templateId) if memberId > 0 { qs = qs.Filter("member_id", memberId) } _, err := qs.Delete() if err != nil { logs.Error("删除模板失败 ->", err) } return err } //添加或更新模板 func (t *Template) Save(cols ...string) (err error) { if t.BookId <= 0 { return ErrInvalidParameter } o := orm.NewOrm() if !o.QueryTable(NewBook().TableNameWithPrefix()).Filter("book_id", t.BookId).Exist() { return errors.New("项目不存在") } if !o.QueryTable(NewMember().TableNameWithPrefix()).Filter("member_id", t.MemberId).Filter("status", 0).Exist() { return errors.New("用户已被禁用") } t.Version = time.Now().Unix() if t.TemplateId > 0 { t.ModifyTime = time.Now() _, err = o.Update(t, cols...) } else { t.CreateTime = time.Now() _, err = o.Insert(t) } return } //预加载一些数据 func (t *Template) Preload() *Template { if t != nil { if t.MemberId > 0 { m, err := NewMember().Find(t.MemberId, "account", "real_name") if err == nil { if m.RealName != "" { t.CreateName = m.RealName } else { t.CreateName = m.Account } } else { logs.Error("加载模板所有者失败 ->", err) } } if t.ModifyAt > 0 { if m, err := NewMember().Find(t.ModifyAt, "account", "real_name"); err == nil { if m.RealName != "" { t.ModifyName = m.RealName } else { t.ModifyName = m.Account } } } if t.BookId > 0 { if b, err := NewBook().Find(t.BookId, "book_name"); err == nil { t.BookName = b.BookName } } } return t } ================================================ FILE: models/comment_result.go ================================================ package models import "github.com/beego/beego/v2/client/orm" type CommentResult struct { Comment Author string `json:"author"` ReplyAccount string `json:"reply_account"` } func (m *CommentResult) FindForDocumentToPager(doc_id, page_index, page_size int) (comments []*CommentResult, totalCount int, err error) { o := orm.NewOrm() sql1 := ` SELECT comment.* , parent.* , mdmb.account AS author, p_member.account AS reply_account FROM md_comments AS comment LEFT JOIN md_members AS mdmb ON comment.member_id = mdmb.member_id LEFT JOIN md_comments AS parent ON comment.parent_id = parent.comment_id LEFT JOIN md_members AS p_member ON p_member.member_id = parent.member_id WHERE comment.document_id = ? ORDER BY comment.comment_id DESC LIMIT 0,10` offset := (page_index - 1) * page_size _, err = o.Raw(sql1, doc_id, offset, page_size).QueryRows(&comments) v, err := o.QueryTable(m.TableNameWithPrefix()).Filter("document_id", doc_id).Count() if err == nil { totalCount = int(v) } return } ================================================ FILE: models/comment_vote.go ================================================ package models import ( "time" "github.com/beego/beego/v2/client/orm" "github.com/mindoc-org/mindoc/conf" ) type CommentVote struct { VoteId int `orm:"column(vote_id);pk;auto;unique" json:"vote_id"` CommentId int `orm:"column(comment_id);type(int);index" json:"comment_id"` CommentMemberId int `orm:"column(comment_member_id);type(int);index;default(0)" json:"comment_member_id"` VoteMemberId int `orm:"column(vote_member_id);type(int);index" json:"vote_member_id"` VoteState int `orm:"column(vote_state);type(int)" json:"vote_state"` CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` } // TableName 获取对应数据库表名. func (m *CommentVote) TableName() string { return "comment_votes" } // TableEngine 获取数据使用的引擎. func (m *CommentVote) TableEngine() string { return "INNODB" } func (m *CommentVote) TableNameWithPrefix() string { return conf.GetDatabasePrefix() + m.TableName() } func (u *CommentVote) TableUnique() [][]string { return [][]string{ []string{"comment_id", "vote_member_id"}, } } func NewCommentVote() *CommentVote { return &CommentVote{} } func (m *CommentVote) InsertOrUpdate() (*CommentVote, error) { o := orm.NewOrm() if m.VoteId > 0 { _, err := o.Update(m) return m, err } else { _, err := o.Insert(m) return m, err } } ================================================ FILE: routers/filter.go ================================================ package routers import ( "encoding/json" "net/url" "regexp" "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web/context" "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/mcp" "github.com/mindoc-org/mindoc/models" ) func init() { var FilterUser = func(ctx *context.Context) { _, ok := ctx.Input.Session(conf.LoginSessionName).(models.Member) if !ok { if ctx.Input.IsAjax() { jsonData := make(map[string]interface{}, 3) jsonData["errcode"] = 403 jsonData["message"] = "请登录后再操作" returnJSON, _ := json.Marshal(jsonData) ctx.ResponseWriter.Write(returnJSON) } else { ctx.Redirect(302, conf.URLFor("AccountController.Login")+"?url="+url.PathEscape(conf.BaseUrl+ctx.Request.URL.RequestURI())) } } } web.InsertFilter("/manager", web.BeforeRouter, FilterUser) web.InsertFilter("/manager/*", web.BeforeRouter, FilterUser) web.InsertFilter("/setting", web.BeforeRouter, FilterUser) web.InsertFilter("/setting/*", web.BeforeRouter, FilterUser) web.InsertFilter("/book", web.BeforeRouter, FilterUser) web.InsertFilter("/book/*", web.BeforeRouter, FilterUser) web.InsertFilter("/api/*", web.BeforeRouter, FilterUser) web.InsertFilter("/manage/*", web.BeforeRouter, FilterUser) web.InsertFilter("/mcp/*", web.BeforeRouter, mcp.AuthMiddleware) var FinishRouter = func(ctx *context.Context) { ctx.ResponseWriter.Header().Add("MinDoc-Version", conf.VERSION) ctx.ResponseWriter.Header().Add("MinDoc-Site", "https://www.iminho.me") ctx.ResponseWriter.Header().Add("X-XSS-Protection", "1; mode=block") } var StartRouter = func(ctx *context.Context) { sessname, _ := web.AppConfig.String("sessionname") sessionId := ctx.Input.Cookie(sessname) if sessionId != "" { //sessionId必须是数字字母组成,且最小32个字符,最大1024字符 if ok, err := regexp.MatchString(`^[a-zA-Z0-9]{32,512}$`, sessionId); !ok || err != nil { panic("401") } } } web.InsertFilter("/*", web.BeforeStatic, StartRouter, web.WithReturnOnOutput(false)) web.InsertFilter("/*", web.BeforeRouter, FinishRouter, web.WithReturnOnOutput(false)) } ================================================ FILE: routers/router.go ================================================ package routers import ( // "crypto/tls" // "log" "net/http" "net/http/httputil" "net/url" "strings" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web/context" // "github.com/mindoc-org/mindoc/conf" "github.com/mindoc-org/mindoc/controllers" "github.com/mindoc-org/mindoc/mcp" ) type CorsTransport struct { http.RoundTripper } func (t *CorsTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { // refer: https://stackoverflow.com/questions/31535569/golang-how-to-read-response-body-of-reverseproxy/31536962#31536962 resp, err = t.RoundTripper.RoundTrip(req) // beego.Debug(resp) if err != nil { return nil, err } /* b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } err = resp.Body.Close() if err != nil { return nil, err } b = bytes.Replace(b, []byte("server"), []byte("schmerver"), -1) body := ioutil.NopCloser(bytes.NewReader(b)) resp.Body = body resp.ContentLength = int64(len(b)) resp.Header.Set("Content-Length", strconv.Itoa(len(b))) */ // resp.Body.Close() // resp.Header.Del("Access-Control-Request-Method") // resp.Header.Del("Access-Control-Request-Headers") resp.Header.Set("Access-Control-Allow-Origin", "*") resp.Header.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") // resp.Header.Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Requested-With") hs := "" for name, values := range resp.Header { hs = hs + name + ", " _ = values } hs = strings.TrimRight(hs, " ") hs = strings.TrimRight(hs, ",") // beego.Debug(hs) resp.Header.Set("Access-Control-Allow-Headers", hs) resp.Header.Del("Mindoc-Version") resp.Header.Del("Mindoc-Site") resp.Header.Del("Server") resp.Header.Del("X-Xss-Protection") return resp, nil } func singleJoiningSlash(a, b string) string { aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") switch { case aslash && bslash: return a + b[1:] case !aslash && !bslash: return a + "/" + b } return a + b } func init() { web.Any("/hello-any", func(ctx *context.Context) { ctx.Output.Body([]byte("hello any demo")) }) web.Any("/cors-anywhere", func(ctx *context.Context) { u, _ := url.PathUnescape(ctx.Input.Query("url")) if len(u) > 0 && strings.HasPrefix(u, "http") { target, _ := url.Parse(u) if target.Path == ctx.Request.URL.Path { ctx.Output.Body([]byte("")) } else { logs.Error("target: ", target) reverseProxy := httputil.NewSingleHostReverseProxy(target) reverseProxy.Director = func(req *http.Request) { for name, values := range ctx.Request.Header { for _, value := range values { req.Header.Set(name, value) } } req.Header.Add("X-Forwarded-Host", req.Host) req.Header.Add("X-Origin-Host", target.Host) req.URL.Scheme = target.Scheme req.URL.Host = target.Host // proxyPath := singleJoiningSlash(target.Path, req.URL.Path) proxyPath := target.Path if strings.HasSuffix(proxyPath, "/") && len(proxyPath) > 1 { proxyPath = proxyPath[:len(proxyPath)-1] } req.URL.Path = proxyPath } reverseProxy.Transport = &CorsTransport{http.DefaultTransport} reverseProxy.ServeHTTP(ctx.ResponseWriter, ctx.Request) panic(web.ErrAbort) } } else { ctx.ResponseWriter.WriteHeader(http.StatusBadRequest) ctx.Output.Body([]byte("400 Bad Request")) } }) web.Router("/", &controllers.HomeController{}, "*:Index") web.Router("/login", &controllers.AccountController{}, "*:Login") web.Router("/auth2/redirect/:app", &controllers.AccountController{}, "*:Auth2Redirect") web.Router("/auth2/callback/:app", &controllers.AccountController{}, "*:Auth2Callback") web.Router("/auth2/account/bind/:app", &controllers.AccountController{}, "*:Auth2BindAccount") web.Router("/auth2/account/auto/:app", &controllers.AccountController{}, "*:Auth2AutoAccount") //web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin") //web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin") web.Router("/logout", &controllers.AccountController{}, "*:Logout") web.Router("/register", &controllers.AccountController{}, "*:Register") web.Router("/find_password", &controllers.AccountController{}, "*:FindPassword") web.Router("/valid_email", &controllers.AccountController{}, "post:ValidEmail") web.Router("/captcha", &controllers.AccountController{}, "*:Captcha") web.Router("/manager", &controllers.ManagerController{}, "*:Index") web.Router("/manager/users", &controllers.ManagerController{}, "*:Users") web.Router("/manager/users/edit/:id", &controllers.ManagerController{}, "*:EditMember") web.Router("/manager/member/create", &controllers.ManagerController{}, "post:CreateMember") web.Router("/manager/member/delete", &controllers.ManagerController{}, "post:DeleteMember") web.Router("/manager/member/update-member-status", &controllers.ManagerController{}, "post:UpdateMemberStatus") web.Router("/manager/member/change-member-role", &controllers.ManagerController{}, "post:ChangeMemberRole") web.Router("/manager/books", &controllers.ManagerController{}, "*:Books") web.Router("/manager/books/edit/:key", &controllers.ManagerController{}, "*:EditBook") web.Router("/manager/books/delete", &controllers.ManagerController{}, "*:DeleteBook") web.Router("/manager/comments", &controllers.ManagerController{}, "*:Comments") web.Router("/manager/setting", &controllers.ManagerController{}, "*:Setting") web.Router("/manager/books/token", &controllers.ManagerController{}, "post:CreateToken") web.Router("/manager/books/transfer", &controllers.ManagerController{}, "post:Transfer") web.Router("/manager/books/open", &controllers.ManagerController{}, "post:PrivatelyOwned") web.Router("/manager/attach/list", &controllers.ManagerController{}, "*:AttachList") web.Router("/manager/attach/clean", &controllers.ManagerController{}, "post:AttachClean") web.Router("/manager/attach/detailed/:id", &controllers.ManagerController{}, "*:AttachDetailed") web.Router("/manager/attach/delete", &controllers.ManagerController{}, "post:AttachDelete") web.Router("/manager/label/list", &controllers.ManagerController{}, "get:LabelList") web.Router("/manager/label/delete/:id", &controllers.ManagerController{}, "post:LabelDelete") //web.Router("/manager/config", &controllers.ManagerController{}, "*:Config") web.Router("/manager/team", &controllers.ManagerController{}, "*:Team") web.Router("/manager/team/create", &controllers.ManagerController{}, "POST:TeamCreate") web.Router("/manager/team/edit", &controllers.ManagerController{}, "POST:TeamEdit") web.Router("/manager/team/delete", &controllers.ManagerController{}, "POST:TeamDelete") web.Router("/manager/team/member/list/:id", &controllers.ManagerController{}, "*:TeamMemberList") web.Router("/manager/team/member/add", &controllers.ManagerController{}, "POST:TeamMemberAdd") web.Router("/manager/team/member/delete", &controllers.ManagerController{}, "POST:TeamMemberDelete") web.Router("/manager/team/member/change_role", &controllers.ManagerController{}, "POST:TeamChangeMemberRole") web.Router("/manager/team/member/search", &controllers.ManagerController{}, "*:TeamSearchMember") web.Router("/manager/team/book/list/:id", &controllers.ManagerController{}, "*:TeamBookList") web.Router("/manager/team/book/add", &controllers.ManagerController{}, "POST:TeamBookAdd") web.Router("/manager/team/book/delete", &controllers.ManagerController{}, "POST:TeamBookDelete") web.Router("/manager/team/book/search", &controllers.ManagerController{}, "*:TeamSearchBook") web.Router("/manager/itemsets", &controllers.ManagerController{}, "*:Itemsets") web.Router("/manager/itemsets/edit", &controllers.ManagerController{}, "post:ItemsetsEdit") web.Router("/manager/itemsets/delete", &controllers.ManagerController{}, "post:ItemsetsDelete") web.Router("/setting", &controllers.SettingController{}, "*:Index") web.Router("/setting/password", &controllers.SettingController{}, "*:Password") web.Router("/setting/upload", &controllers.SettingController{}, "*:Upload") web.Router("/book", &controllers.BookController{}, "*:Index") web.Router("/book/:key/dashboard", &controllers.BookController{}, "*:Dashboard") web.Router("/book/:key/setting", &controllers.BookController{}, "*:Setting") web.Router("/book/:key/users", &controllers.BookController{}, "*:Users") web.Router("/book/:key/release", &controllers.BookController{}, "post:Release") web.Router("/book/:key/sort", &controllers.BookController{}, "post:SaveSort") web.Router("/book/:key/teams", &controllers.BookController{}, "*:Team") web.Router("/book/updatebookorder", &controllers.BookController{}, "post:UpdateBookOrder") web.Router("/book/create", &controllers.BookController{}, "*:Create") web.Router("/book/itemsets/search", &controllers.BookController{}, "*:ItemsetsSearch") web.Router("/book/users/create", &controllers.BookMemberController{}, "post:AddMember") web.Router("/book/users/change", &controllers.BookMemberController{}, "post:ChangeRole") web.Router("/book/users/delete", &controllers.BookMemberController{}, "post:RemoveMember") web.Router("/book/users/import", &controllers.BookController{}, "post:Import") web.Router("/book/users/copy", &controllers.BookController{}, "post:Copy") web.Router("/book/setting/save", &controllers.BookController{}, "post:SaveBook") web.Router("/book/setting/open", &controllers.BookController{}, "post:PrivatelyOwned") web.Router("/book/setting/transfer", &controllers.BookController{}, "post:Transfer") web.Router("/book/setting/upload", &controllers.BookController{}, "post:UploadCover") web.Router("/book/setting/delete", &controllers.BookController{}, "post:Delete") web.Router("/book/team/add", &controllers.BookController{}, "POST:TeamAdd") web.Router("/book/team/delete", &controllers.BookController{}, "POST:TeamDelete") web.Router("/book/team/search", &controllers.BookController{}, "*:TeamSearch") //管理文章的路由 web.Router("/manage/blogs", &controllers.BlogController{}, "*:ManageList") web.Router("/manage/blogs/setting/?:id", &controllers.BlogController{}, "*:ManageSetting") web.Router("/manage/blogs/edit/?:id", &controllers.BlogController{}, "*:ManageEdit") web.Router("/manage/blogs/delete", &controllers.BlogController{}, "post:ManageDelete") web.Router("/manage/blogs/upload", &controllers.BlogController{}, "post:Upload") web.Router("/manage/blogs/attach/:id", &controllers.BlogController{}, "post:RemoveAttachment") //读文章的路由 web.Router("/blogs", &controllers.BlogController{}, "*:List") web.Router("/blog-attach/:id:int/:attach_id:int", &controllers.BlogController{}, "get:Download") web.Router("/blog-:id([0-9]+).html", &controllers.BlogController{}, "*:Index") //模板相关接口 web.Router("/api/template/get", &controllers.TemplateController{}, "get:Get") web.Router("/api/template/list", &controllers.TemplateController{}, "post:List") web.Router("/api/template/add", &controllers.TemplateController{}, "post:Add") web.Router("/api/template/remove", &controllers.TemplateController{}, "post:Delete") web.Router("/api/attach/remove/", &controllers.DocumentController{}, "post:RemoveAttachment") web.Router("/api/:key/edit/?:id", &controllers.DocumentController{}, "*:Edit") web.Router("/api/upload", &controllers.DocumentController{}, "post:Upload") web.Router("/api/:key/create", &controllers.DocumentController{}, "post:Create") web.Router("/api/:key/delete", &controllers.DocumentController{}, "post:Delete") web.Router("/api/:key/content/?:id", &controllers.DocumentController{}, "*:Content") web.Router("/api/:key/compare/:id", &controllers.DocumentController{}, "*:Compare") web.Router("/api/search/user/:key", &controllers.SearchController{}, "*:User") web.Router("/history/get", &controllers.DocumentController{}, "get:History") web.Router("/history/delete", &controllers.DocumentController{}, "*:DeleteHistory") web.Router("/history/restore", &controllers.DocumentController{}, "*:RestoreHistory") web.Router("/docs/:key", &controllers.DocumentController{}, "*:Index") web.Router("/docs/:key/check-password", &controllers.DocumentController{}, "post:CheckPassword") web.Router("/docs/:key/:id", &controllers.DocumentController{}, "*:Read") web.Router("/docs/:key/search", &controllers.DocumentController{}, "post:Search") web.Router("/export/:key", &controllers.DocumentController{}, "*:Export") web.Router("/qrcode/:key.png", &controllers.DocumentController{}, "get:QrCode") web.Router("/attach_files/:key/:attach_id", &controllers.DocumentController{}, "get:DownloadAttachment") web.Router("/comment/create", &controllers.CommentController{}, "post:Create") web.Router("/comment/delete", &controllers.CommentController{}, "post:Delete") web.Router("/comment/lists", &controllers.CommentController{}, "get:Lists") web.Router("/comment/index", &controllers.CommentController{}, "*:Index") web.Router("/search", &controllers.SearchController{}, "get:Index") web.Router("/search-v2", &controllers.SearchController{}, "get:IndexV2") web.Router("/api/search-v2", &controllers.SearchController{}, "get:SearchV2") web.Router("/tag/:key", &controllers.LabelController{}, "get:Index") web.Router("/tags", &controllers.LabelController{}, "get:List") web.Router("/items", &controllers.ItemsetsController{}, "get:Index") web.Router("/items/:key", &controllers.ItemsetsController{}, "get:List") if web.AppConfig.DefaultBool("enable_mcp_server", false) { mcpServer := mcp.NewMCPServer() web.Any("/mcp/*", func(ctx *context.Context) { mcpServer.ServeHTTP().ServeHTTP(ctx.ResponseWriter, ctx.Request) }) } } ================================================ FILE: simsun.ttc ================================================ [File too large to display: 17.4 MB] ================================================ FILE: start.sh ================================================ #!/bin/bash set -eux # 默认资源 if [ ! -d "/mindoc/conf" ]; then mkdir -p "/mindoc/conf" ; fi if [[ -z "$(ls -A -- "/mindoc/conf")" ]] ; then cp -r "/mindoc/__default_assets__/conf" "/mindoc/" ; fi if [ ! -d "/mindoc/static" ]; then mkdir -p "/mindoc/static" ; fi if [[ -z "$(ls -A -- "/mindoc/static")" ]] ; then cp -r "/mindoc/__default_assets__/static" "/mindoc/" ; fi if [ ! -d "/mindoc/views" ]; then mkdir -p "/mindoc/views" ; fi if [[ -z "$(ls -A -- "/mindoc/views")" ]] ; then cp -r "/mindoc/__default_assets__/views" "/mindoc/" ; fi if [ ! -d "/mindoc/uploads" ]; then mkdir -p "/mindoc/uploads" ; fi if [[ -z "$(ls -A -- "/mindoc/uploads")" ]] ; then cp -r "/mindoc/__default_assets__/uploads" "/mindoc/" ; fi # 如果配置文件不存在就复制 cp --no-clobber /mindoc/conf/app.conf.example /mindoc/conf/app.conf # 数据库等初始化 /mindoc/mindoc_linux_amd64 install # 运行 /mindoc/mindoc_linux_amd64 # # Debug Dockerfile # while [ 1 ] # do # echo "log ..." # sleep 5s # done ================================================ FILE: static/bootstrap/css/bootstrap-theme.css ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ .btn-default, .btn-primary, .btn-success, .btn-info, .btn-warning, .btn-danger { text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); } .btn-default:active, .btn-primary:active, .btn-success:active, .btn-info:active, .btn-warning:active, .btn-danger:active, .btn-default.active, .btn-primary.active, .btn-success.active, .btn-info.active, .btn-warning.active, .btn-danger.active { -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn-default.disabled, .btn-primary.disabled, .btn-success.disabled, .btn-info.disabled, .btn-warning.disabled, .btn-danger.disabled, .btn-default[disabled], .btn-primary[disabled], .btn-success[disabled], .btn-info[disabled], .btn-warning[disabled], .btn-danger[disabled], fieldset[disabled] .btn-default, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-success, fieldset[disabled] .btn-info, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-danger { -webkit-box-shadow: none; box-shadow: none; } .btn-default .badge, .btn-primary .badge, .btn-success .badge, .btn-info .badge, .btn-warning .badge, .btn-danger .badge { text-shadow: none; } .btn:active, .btn.active { background-image: none; } .btn-default { text-shadow: 0 1px 0 #fff; background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #dbdbdb; border-color: #ccc; } .btn-default:hover, .btn-default:focus { background-color: #e0e0e0; background-position: 0 -15px; } .btn-default:active, .btn-default.active { background-color: #e0e0e0; border-color: #dbdbdb; } .btn-default.disabled, .btn-default[disabled], fieldset[disabled] .btn-default, .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus, .btn-default.disabled:active, .btn-default[disabled]:active, fieldset[disabled] .btn-default:active, .btn-default.disabled.active, .btn-default[disabled].active, fieldset[disabled] .btn-default.active { background-color: #e0e0e0; background-image: none; } .btn-primary { background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #245580; } .btn-primary:hover, .btn-primary:focus { background-color: #265a88; background-position: 0 -15px; } .btn-primary:active, .btn-primary.active { background-color: #265a88; border-color: #245580; } .btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled.focus, .btn-primary[disabled].focus, fieldset[disabled] .btn-primary.focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #265a88; background-image: none; } .btn-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #3e8f3e; } .btn-success:hover, .btn-success:focus { background-color: #419641; background-position: 0 -15px; } .btn-success:active, .btn-success.active { background-color: #419641; border-color: #3e8f3e; } .btn-success.disabled, .btn-success[disabled], fieldset[disabled] .btn-success, .btn-success.disabled:hover, .btn-success[disabled]:hover, fieldset[disabled] .btn-success:hover, .btn-success.disabled:focus, .btn-success[disabled]:focus, fieldset[disabled] .btn-success:focus, .btn-success.disabled.focus, .btn-success[disabled].focus, fieldset[disabled] .btn-success.focus, .btn-success.disabled:active, .btn-success[disabled]:active, fieldset[disabled] .btn-success:active, .btn-success.disabled.active, .btn-success[disabled].active, fieldset[disabled] .btn-success.active { background-color: #419641; background-image: none; } .btn-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #28a4c9; } .btn-info:hover, .btn-info:focus { background-color: #2aabd2; background-position: 0 -15px; } .btn-info:active, .btn-info.active { background-color: #2aabd2; border-color: #28a4c9; } .btn-info.disabled, .btn-info[disabled], fieldset[disabled] .btn-info, .btn-info.disabled:hover, .btn-info[disabled]:hover, fieldset[disabled] .btn-info:hover, .btn-info.disabled:focus, .btn-info[disabled]:focus, fieldset[disabled] .btn-info:focus, .btn-info.disabled.focus, .btn-info[disabled].focus, fieldset[disabled] .btn-info.focus, .btn-info.disabled:active, .btn-info[disabled]:active, fieldset[disabled] .btn-info:active, .btn-info.disabled.active, .btn-info[disabled].active, fieldset[disabled] .btn-info.active { background-color: #2aabd2; background-image: none; } .btn-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #e38d13; } .btn-warning:hover, .btn-warning:focus { background-color: #eb9316; background-position: 0 -15px; } .btn-warning:active, .btn-warning.active { background-color: #eb9316; border-color: #e38d13; } .btn-warning.disabled, .btn-warning[disabled], fieldset[disabled] .btn-warning, .btn-warning.disabled:hover, .btn-warning[disabled]:hover, fieldset[disabled] .btn-warning:hover, .btn-warning.disabled:focus, .btn-warning[disabled]:focus, fieldset[disabled] .btn-warning:focus, .btn-warning.disabled.focus, .btn-warning[disabled].focus, fieldset[disabled] .btn-warning.focus, .btn-warning.disabled:active, .btn-warning[disabled]:active, fieldset[disabled] .btn-warning:active, .btn-warning.disabled.active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning.active { background-color: #eb9316; background-image: none; } .btn-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-color: #b92c28; } .btn-danger:hover, .btn-danger:focus { background-color: #c12e2a; background-position: 0 -15px; } .btn-danger:active, .btn-danger.active { background-color: #c12e2a; border-color: #b92c28; } .btn-danger.disabled, .btn-danger[disabled], fieldset[disabled] .btn-danger, .btn-danger.disabled:hover, .btn-danger[disabled]:hover, fieldset[disabled] .btn-danger:hover, .btn-danger.disabled:focus, .btn-danger[disabled]:focus, fieldset[disabled] .btn-danger:focus, .btn-danger.disabled.focus, .btn-danger[disabled].focus, fieldset[disabled] .btn-danger.focus, .btn-danger.disabled:active, .btn-danger[disabled]:active, fieldset[disabled] .btn-danger:active, .btn-danger.disabled.active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger.active { background-color: #c12e2a; background-image: none; } .thumbnail, .img-thumbnail { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); box-shadow: 0 1px 2px rgba(0, 0, 0, .075); } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { background-color: #e8e8e8; background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-repeat: repeat-x; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { background-color: #2e6da4; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } .navbar-default { background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); } .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a { background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); background-repeat: repeat-x; -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); } .navbar-brand, .navbar-nav > li > a { text-shadow: 0 1px 0 rgba(255, 255, 255, .25); } .navbar-inverse { background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; border-radius: 4px; } .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .active > a { background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); background-repeat: repeat-x; -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); } .navbar-inverse .navbar-brand, .navbar-inverse .navbar-nav > li > a { text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); } .navbar-static-top, .navbar-fixed-top, .navbar-fixed-bottom { border-radius: 0; } @media (max-width: 767px) { .navbar .navbar-nav .open .dropdown-menu > .active > a, .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { color: #fff; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } } .alert { text-shadow: 0 1px 0 rgba(255, 255, 255, .2); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); } .alert-success { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); background-repeat: repeat-x; border-color: #b2dba1; } .alert-info { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); background-repeat: repeat-x; border-color: #9acfea; } .alert-warning { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); background-repeat: repeat-x; border-color: #f5e79e; } .alert-danger { background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); background-repeat: repeat-x; border-color: #dca7a7; } .progress { background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); background-repeat: repeat-x; } .progress-bar { background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); background-repeat: repeat-x; } .progress-bar-success { background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); background-repeat: repeat-x; } .progress-bar-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); background-repeat: repeat-x; } .progress-bar-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); background-repeat: repeat-x; } .progress-bar-danger { background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); background-repeat: repeat-x; } .progress-bar-striped { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .list-group { border-radius: 4px; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); box-shadow: 0 1px 2px rgba(0, 0, 0, .075); } .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { text-shadow: 0 -1px 0 #286090; background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); background-repeat: repeat-x; border-color: #2b669a; } .list-group-item.active .badge, .list-group-item.active:hover .badge, .list-group-item.active:focus .badge { text-shadow: none; } .panel { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); box-shadow: 0 1px 2px rgba(0, 0, 0, .05); } .panel-default > .panel-heading { background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); background-repeat: repeat-x; } .panel-primary > .panel-heading { background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); background-repeat: repeat-x; } .panel-success > .panel-heading { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); background-repeat: repeat-x; } .panel-info > .panel-heading { background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); background-repeat: repeat-x; } .panel-warning > .panel-heading { background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); background-repeat: repeat-x; } .panel-danger > .panel-heading { background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); background-repeat: repeat-x; } .well { background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); background-repeat: repeat-x; border-color: #dcdcdc; -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); } /*# sourceMappingURL=bootstrap-theme.css.map */ ================================================ FILE: static/bootstrap/css/bootstrap.css ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } body { margin: 0; } article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } audio, canvas, progress, video { display: inline-block; vertical-align: baseline; } audio:not([controls]) { display: none; height: 0; } [hidden], template { display: none; } a { background-color: transparent; } a:active, a:hover { outline: 0; } abbr[title] { border-bottom: 1px dotted; } b, strong { font-weight: bold; } dfn { font-style: italic; } h1 { margin: .67em 0; font-size: 2em; } mark { color: #000; background: #ff0; } small { font-size: 80%; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sup { top: -.5em; } sub { bottom: -.25em; } img { border: 0; } svg:not(:root) { overflow: hidden; } figure { margin: 1em 40px; } hr { height: 0; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } pre { overflow: auto; } code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em; } button, input, optgroup, select, textarea { margin: 0; font: inherit; color: inherit; } button { overflow: visible; } button, select { text-transform: none; } button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; cursor: pointer; } button[disabled], html input[disabled] { cursor: default; } button::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; } input { line-height: normal; } input[type="checkbox"], input[type="radio"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 0; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { height: auto; } input[type="search"] { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -webkit-appearance: textfield; } input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } fieldset { padding: .35em .625em .75em; margin: 0 2px; border: 1px solid #c0c0c0; } legend { padding: 0; border: 0; } textarea { overflow: auto; } optgroup { font-weight: bold; } table { border-spacing: 0; border-collapse: collapse; } td, th { padding: 0; } /*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ @media print { *, *:before, *:after { color: #000 !important; text-shadow: none !important; background: transparent !important; -webkit-box-shadow: none !important; box-shadow: none !important; } a, a:visited { text-decoration: underline; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } a[href^="#"]:after, a[href^="javascript:"]:after { content: ""; } pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } .navbar { display: none; } .btn > .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordered td { border: 1px solid #ddd !important; } } @font-face { font-family: 'Glyphicons Halflings'; src: url('../fonts/glyphicons-halflings-regular.eot'); src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); } .glyphicon { position: relative; top: 1px; display: inline-block; font-family: 'Glyphicons Halflings'; font-style: normal; font-weight: normal; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .glyphicon-asterisk:before { content: "\002a"; } .glyphicon-plus:before { content: "\002b"; } .glyphicon-euro:before, .glyphicon-eur:before { content: "\20ac"; } .glyphicon-minus:before { content: "\2212"; } .glyphicon-cloud:before { content: "\2601"; } .glyphicon-envelope:before { content: "\2709"; } .glyphicon-pencil:before { content: "\270f"; } .glyphicon-glass:before { content: "\e001"; } .glyphicon-music:before { content: "\e002"; } .glyphicon-search:before { content: "\e003"; } .glyphicon-heart:before { content: "\e005"; } .glyphicon-star:before { content: "\e006"; } .glyphicon-star-empty:before { content: "\e007"; } .glyphicon-user:before { content: "\e008"; } .glyphicon-film:before { content: "\e009"; } .glyphicon-th-large:before { content: "\e010"; } .glyphicon-th:before { content: "\e011"; } .glyphicon-th-list:before { content: "\e012"; } .glyphicon-ok:before { content: "\e013"; } .glyphicon-remove:before { content: "\e014"; } .glyphicon-zoom-in:before { content: "\e015"; } .glyphicon-zoom-out:before { content: "\e016"; } .glyphicon-off:before { content: "\e017"; } .glyphicon-signal:before { content: "\e018"; } .glyphicon-cog:before { content: "\e019"; } .glyphicon-trash:before { content: "\e020"; } .glyphicon-home:before { content: "\e021"; } .glyphicon-file:before { content: "\e022"; } .glyphicon-time:before { content: "\e023"; } .glyphicon-road:before { content: "\e024"; } .glyphicon-download-alt:before { content: "\e025"; } .glyphicon-download:before { content: "\e026"; } .glyphicon-upload:before { content: "\e027"; } .glyphicon-inbox:before { content: "\e028"; } .glyphicon-play-circle:before { content: "\e029"; } .glyphicon-repeat:before { content: "\e030"; } .glyphicon-refresh:before { content: "\e031"; } .glyphicon-list-alt:before { content: "\e032"; } .glyphicon-lock:before { content: "\e033"; } .glyphicon-flag:before { content: "\e034"; } .glyphicon-headphones:before { content: "\e035"; } .glyphicon-volume-off:before { content: "\e036"; } .glyphicon-volume-down:before { content: "\e037"; } .glyphicon-volume-up:before { content: "\e038"; } .glyphicon-qrcode:before { content: "\e039"; } .glyphicon-barcode:before { content: "\e040"; } .glyphicon-tag:before { content: "\e041"; } .glyphicon-tags:before { content: "\e042"; } .glyphicon-book:before { content: "\e043"; } .glyphicon-bookmark:before { content: "\e044"; } .glyphicon-print:before { content: "\e045"; } .glyphicon-camera:before { content: "\e046"; } .glyphicon-font:before { content: "\e047"; } .glyphicon-bold:before { content: "\e048"; } .glyphicon-italic:before { content: "\e049"; } .glyphicon-text-height:before { content: "\e050"; } .glyphicon-text-width:before { content: "\e051"; } .glyphicon-align-left:before { content: "\e052"; } .glyphicon-align-center:before { content: "\e053"; } .glyphicon-align-right:before { content: "\e054"; } .glyphicon-align-justify:before { content: "\e055"; } .glyphicon-list:before { content: "\e056"; } .glyphicon-indent-left:before { content: "\e057"; } .glyphicon-indent-right:before { content: "\e058"; } .glyphicon-facetime-video:before { content: "\e059"; } .glyphicon-picture:before { content: "\e060"; } .glyphicon-map-marker:before { content: "\e062"; } .glyphicon-adjust:before { content: "\e063"; } .glyphicon-tint:before { content: "\e064"; } .glyphicon-edit:before { content: "\e065"; } .glyphicon-share:before { content: "\e066"; } .glyphicon-check:before { content: "\e067"; } .glyphicon-move:before { content: "\e068"; } .glyphicon-step-backward:before { content: "\e069"; } .glyphicon-fast-backward:before { content: "\e070"; } .glyphicon-backward:before { content: "\e071"; } .glyphicon-play:before { content: "\e072"; } .glyphicon-pause:before { content: "\e073"; } .glyphicon-stop:before { content: "\e074"; } .glyphicon-forward:before { content: "\e075"; } .glyphicon-fast-forward:before { content: "\e076"; } .glyphicon-step-forward:before { content: "\e077"; } .glyphicon-eject:before { content: "\e078"; } .glyphicon-chevron-left:before { content: "\e079"; } .glyphicon-chevron-right:before { content: "\e080"; } .glyphicon-plus-sign:before { content: "\e081"; } .glyphicon-minus-sign:before { content: "\e082"; } .glyphicon-remove-sign:before { content: "\e083"; } .glyphicon-ok-sign:before { content: "\e084"; } .glyphicon-question-sign:before { content: "\e085"; } .glyphicon-info-sign:before { content: "\e086"; } .glyphicon-screenshot:before { content: "\e087"; } .glyphicon-remove-circle:before { content: "\e088"; } .glyphicon-ok-circle:before { content: "\e089"; } .glyphicon-ban-circle:before { content: "\e090"; } .glyphicon-arrow-left:before { content: "\e091"; } .glyphicon-arrow-right:before { content: "\e092"; } .glyphicon-arrow-up:before { content: "\e093"; } .glyphicon-arrow-down:before { content: "\e094"; } .glyphicon-share-alt:before { content: "\e095"; } .glyphicon-resize-full:before { content: "\e096"; } .glyphicon-resize-small:before { content: "\e097"; } .glyphicon-exclamation-sign:before { content: "\e101"; } .glyphicon-gift:before { content: "\e102"; } .glyphicon-leaf:before { content: "\e103"; } .glyphicon-fire:before { content: "\e104"; } .glyphicon-eye-open:before { content: "\e105"; } .glyphicon-eye-close:before { content: "\e106"; } .glyphicon-warning-sign:before { content: "\e107"; } .glyphicon-plane:before { content: "\e108"; } .glyphicon-calendar:before { content: "\e109"; } .glyphicon-random:before { content: "\e110"; } .glyphicon-comment:before { content: "\e111"; } .glyphicon-magnet:before { content: "\e112"; } .glyphicon-chevron-up:before { content: "\e113"; } .glyphicon-chevron-down:before { content: "\e114"; } .glyphicon-retweet:before { content: "\e115"; } .glyphicon-shopping-cart:before { content: "\e116"; } .glyphicon-folder-close:before { content: "\e117"; } .glyphicon-folder-open:before { content: "\e118"; } .glyphicon-resize-vertical:before { content: "\e119"; } .glyphicon-resize-horizontal:before { content: "\e120"; } .glyphicon-hdd:before { content: "\e121"; } .glyphicon-bullhorn:before { content: "\e122"; } .glyphicon-bell:before { content: "\e123"; } .glyphicon-certificate:before { content: "\e124"; } .glyphicon-thumbs-up:before { content: "\e125"; } .glyphicon-thumbs-down:before { content: "\e126"; } .glyphicon-hand-right:before { content: "\e127"; } .glyphicon-hand-left:before { content: "\e128"; } .glyphicon-hand-up:before { content: "\e129"; } .glyphicon-hand-down:before { content: "\e130"; } .glyphicon-circle-arrow-right:before { content: "\e131"; } .glyphicon-circle-arrow-left:before { content: "\e132"; } .glyphicon-circle-arrow-up:before { content: "\e133"; } .glyphicon-circle-arrow-down:before { content: "\e134"; } .glyphicon-globe:before { content: "\e135"; } .glyphicon-wrench:before { content: "\e136"; } .glyphicon-tasks:before { content: "\e137"; } .glyphicon-filter:before { content: "\e138"; } .glyphicon-briefcase:before { content: "\e139"; } .glyphicon-fullscreen:before { content: "\e140"; } .glyphicon-dashboard:before { content: "\e141"; } .glyphicon-paperclip:before { content: "\e142"; } .glyphicon-heart-empty:before { content: "\e143"; } .glyphicon-link:before { content: "\e144"; } .glyphicon-phone:before { content: "\e145"; } .glyphicon-pushpin:before { content: "\e146"; } .glyphicon-usd:before { content: "\e148"; } .glyphicon-gbp:before { content: "\e149"; } .glyphicon-sort:before { content: "\e150"; } .glyphicon-sort-by-alphabet:before { content: "\e151"; } .glyphicon-sort-by-alphabet-alt:before { content: "\e152"; } .glyphicon-sort-by-order:before { content: "\e153"; } .glyphicon-sort-by-order-alt:before { content: "\e154"; } .glyphicon-sort-by-attributes:before { content: "\e155"; } .glyphicon-sort-by-attributes-alt:before { content: "\e156"; } .glyphicon-unchecked:before { content: "\e157"; } .glyphicon-expand:before { content: "\e158"; } .glyphicon-collapse-down:before { content: "\e159"; } .glyphicon-collapse-up:before { content: "\e160"; } .glyphicon-log-in:before { content: "\e161"; } .glyphicon-flash:before { content: "\e162"; } .glyphicon-log-out:before { content: "\e163"; } .glyphicon-new-window:before { content: "\e164"; } .glyphicon-record:before { content: "\e165"; } .glyphicon-save:before { content: "\e166"; } .glyphicon-open:before { content: "\e167"; } .glyphicon-saved:before { content: "\e168"; } .glyphicon-import:before { content: "\e169"; } .glyphicon-export:before { content: "\e170"; } .glyphicon-send:before { content: "\e171"; } .glyphicon-floppy-disk:before { content: "\e172"; } .glyphicon-floppy-saved:before { content: "\e173"; } .glyphicon-floppy-remove:before { content: "\e174"; } .glyphicon-floppy-save:before { content: "\e175"; } .glyphicon-floppy-open:before { content: "\e176"; } .glyphicon-credit-card:before { content: "\e177"; } .glyphicon-transfer:before { content: "\e178"; } .glyphicon-cutlery:before { content: "\e179"; } .glyphicon-header:before { content: "\e180"; } .glyphicon-compressed:before { content: "\e181"; } .glyphicon-earphone:before { content: "\e182"; } .glyphicon-phone-alt:before { content: "\e183"; } .glyphicon-tower:before { content: "\e184"; } .glyphicon-stats:before { content: "\e185"; } .glyphicon-sd-video:before { content: "\e186"; } .glyphicon-hd-video:before { content: "\e187"; } .glyphicon-subtitles:before { content: "\e188"; } .glyphicon-sound-stereo:before { content: "\e189"; } .glyphicon-sound-dolby:before { content: "\e190"; } .glyphicon-sound-5-1:before { content: "\e191"; } .glyphicon-sound-6-1:before { content: "\e192"; } .glyphicon-sound-7-1:before { content: "\e193"; } .glyphicon-copyright-mark:before { content: "\e194"; } .glyphicon-registration-mark:before { content: "\e195"; } .glyphicon-cloud-download:before { content: "\e197"; } .glyphicon-cloud-upload:before { content: "\e198"; } .glyphicon-tree-conifer:before { content: "\e199"; } .glyphicon-tree-deciduous:before { content: "\e200"; } .glyphicon-cd:before { content: "\e201"; } .glyphicon-save-file:before { content: "\e202"; } .glyphicon-open-file:before { content: "\e203"; } .glyphicon-level-up:before { content: "\e204"; } .glyphicon-copy:before { content: "\e205"; } .glyphicon-paste:before { content: "\e206"; } .glyphicon-alert:before { content: "\e209"; } .glyphicon-equalizer:before { content: "\e210"; } .glyphicon-king:before { content: "\e211"; } .glyphicon-queen:before { content: "\e212"; } .glyphicon-pawn:before { content: "\e213"; } .glyphicon-bishop:before { content: "\e214"; } .glyphicon-knight:before { content: "\e215"; } .glyphicon-baby-formula:before { content: "\e216"; } .glyphicon-tent:before { content: "\26fa"; } .glyphicon-blackboard:before { content: "\e218"; } .glyphicon-bed:before { content: "\e219"; } .glyphicon-apple:before { content: "\f8ff"; } .glyphicon-erase:before { content: "\e221"; } .glyphicon-hourglass:before { content: "\231b"; } .glyphicon-lamp:before { content: "\e223"; } .glyphicon-duplicate:before { content: "\e224"; } .glyphicon-piggy-bank:before { content: "\e225"; } .glyphicon-scissors:before { content: "\e226"; } .glyphicon-bitcoin:before { content: "\e227"; } .glyphicon-btc:before { content: "\e227"; } .glyphicon-xbt:before { content: "\e227"; } .glyphicon-yen:before { content: "\00a5"; } .glyphicon-jpy:before { content: "\00a5"; } .glyphicon-ruble:before { content: "\20bd"; } .glyphicon-rub:before { content: "\20bd"; } .glyphicon-scale:before { content: "\e230"; } .glyphicon-ice-lolly:before { content: "\e231"; } .glyphicon-ice-lolly-tasted:before { content: "\e232"; } .glyphicon-education:before { content: "\e233"; } .glyphicon-option-horizontal:before { content: "\e234"; } .glyphicon-option-vertical:before { content: "\e235"; } .glyphicon-menu-hamburger:before { content: "\e236"; } .glyphicon-modal-window:before { content: "\e237"; } .glyphicon-oil:before { content: "\e238"; } .glyphicon-grain:before { content: "\e239"; } .glyphicon-sunglasses:before { content: "\e240"; } .glyphicon-text-size:before { content: "\e241"; } .glyphicon-text-color:before { content: "\e242"; } .glyphicon-text-background:before { content: "\e243"; } .glyphicon-object-align-top:before { content: "\e244"; } .glyphicon-object-align-bottom:before { content: "\e245"; } .glyphicon-object-align-horizontal:before { content: "\e246"; } .glyphicon-object-align-left:before { content: "\e247"; } .glyphicon-object-align-vertical:before { content: "\e248"; } .glyphicon-object-align-right:before { content: "\e249"; } .glyphicon-triangle-right:before { content: "\e250"; } .glyphicon-triangle-left:before { content: "\e251"; } .glyphicon-triangle-bottom:before { content: "\e252"; } .glyphicon-triangle-top:before { content: "\e253"; } .glyphicon-console:before { content: "\e254"; } .glyphicon-superscript:before { content: "\e255"; } .glyphicon-subscript:before { content: "\e256"; } .glyphicon-menu-left:before { content: "\e257"; } .glyphicon-menu-right:before { content: "\e258"; } .glyphicon-menu-down:before { content: "\e259"; } .glyphicon-menu-up:before { content: "\e260"; } * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } html { font-size: 10px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.42857143; color: #333; background-color: #fff; } input, button, select, textarea { font-family: inherit; font-size: inherit; line-height: inherit; } a { color: #337ab7; text-decoration: none; } a:hover, a:focus { color: #23527c; text-decoration: underline; } a:focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } figure { margin: 0; } img { vertical-align: middle; } .img-responsive, .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, .carousel-inner > .item > a > img { display: block; max-width: 100%; height: auto; } .img-rounded { border-radius: 6px; } .img-thumbnail { display: inline-block; max-width: 100%; height: auto; padding: 4px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: all .2s ease-in-out; -o-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } .img-circle { border-radius: 50%; } hr { margin-top: 20px; margin-bottom: 20px; border: 0; border-top: 1px solid #eee; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; } [role="button"] { cursor: pointer; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { font-family: inherit; font-weight: 500; line-height: 1.1; color: inherit; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small, .h1 small, .h2 small, .h3 small, .h4 small, .h5 small, .h6 small, h1 .small, h2 .small, h3 .small, h4 .small, h5 .small, h6 .small, .h1 .small, .h2 .small, .h3 .small, .h4 .small, .h5 .small, .h6 .small { font-weight: normal; line-height: 1; color: #777; } h1, .h1, h2, .h2, h3, .h3 { margin-top: 20px; margin-bottom: 10px; } h1 small, .h1 small, h2 small, .h2 small, h3 small, .h3 small, h1 .small, .h1 .small, h2 .small, .h2 .small, h3 .small, .h3 .small { font-size: 65%; } h4, .h4, h5, .h5, h6, .h6 { margin-top: 10px; margin-bottom: 10px; } h4 small, .h4 small, h5 small, .h5 small, h6 small, .h6 small, h4 .small, .h4 .small, h5 .small, .h5 .small, h6 .small, .h6 .small { font-size: 75%; } h1, .h1 { font-size: 36px; } h2, .h2 { font-size: 30px; } h3, .h3 { font-size: 24px; } h4, .h4 { font-size: 18px; } h5, .h5 { font-size: 14px; } h6, .h6 { font-size: 12px; } p { margin: 0 0 10px; } .lead { margin-bottom: 20px; font-size: 16px; font-weight: 300; line-height: 1.4; } @media (min-width: 768px) { .lead { font-size: 21px; } } small, .small { font-size: 85%; } mark, .mark { padding: .2em; background-color: #fcf8e3; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } .text-justify { text-align: justify; } .text-nowrap { white-space: nowrap; } .text-lowercase { text-transform: lowercase; } .text-uppercase { text-transform: uppercase; } .text-capitalize { text-transform: capitalize; } .text-muted { color: #777; } .text-primary { color: #337ab7; } a.text-primary:hover, a.text-primary:focus { color: #286090; } .text-success { color: #3c763d; } a.text-success:hover, a.text-success:focus { color: #2b542c; } .text-info { color: #31708f; } a.text-info:hover, a.text-info:focus { color: #245269; } .text-warning { color: #8a6d3b; } a.text-warning:hover, a.text-warning:focus { color: #66512c; } .text-danger { color: #a94442; } a.text-danger:hover, a.text-danger:focus { color: #843534; } .bg-primary { color: #fff; background-color: #337ab7; } a.bg-primary:hover, a.bg-primary:focus { background-color: #286090; } .bg-success { background-color: #dff0d8; } a.bg-success:hover, a.bg-success:focus { background-color: #c1e2b3; } .bg-info { background-color: #d9edf7; } a.bg-info:hover, a.bg-info:focus { background-color: #afd9ee; } .bg-warning { background-color: #fcf8e3; } a.bg-warning:hover, a.bg-warning:focus { background-color: #f7ecb5; } .bg-danger { background-color: #f2dede; } a.bg-danger:hover, a.bg-danger:focus { background-color: #e4b9b9; } .page-header { padding-bottom: 9px; margin: 40px 0 20px; border-bottom: 1px solid #eee; } ul, ol { margin-top: 0; margin-bottom: 10px; } ul ul, ol ul, ul ol, ol ol { margin-bottom: 0; } .list-unstyled { padding-left: 0; list-style: none; } .list-inline { padding-left: 0; margin-left: -5px; list-style: none; } .list-inline > li { display: inline-block; padding-right: 5px; padding-left: 5px; } dl { margin-top: 0; margin-bottom: 20px; } dt, dd { line-height: 1.42857143; } dt { font-weight: bold; } dd { margin-left: 0; } @media (min-width: 768px) { .dl-horizontal dt { float: left; width: 160px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } .dl-horizontal dd { margin-left: 180px; } } abbr[title], abbr[data-original-title] { cursor: help; border-bottom: 1px dotted #777; } .initialism { font-size: 90%; text-transform: uppercase; } blockquote { padding: 10px 20px; margin: 0 0 20px; font-size: 17.5px; border-left: 5px solid #eee; } blockquote p:last-child, blockquote ul:last-child, blockquote ol:last-child { margin-bottom: 0; } blockquote footer, blockquote small, blockquote .small { display: block; font-size: 80%; line-height: 1.42857143; color: #777; } blockquote footer:before, blockquote small:before, blockquote .small:before { content: '\2014 \00A0'; } .blockquote-reverse, blockquote.pull-right { padding-right: 15px; padding-left: 0; text-align: right; border-right: 5px solid #eee; border-left: 0; } .blockquote-reverse footer:before, blockquote.pull-right footer:before, .blockquote-reverse small:before, blockquote.pull-right small:before, .blockquote-reverse .small:before, blockquote.pull-right .small:before { content: ''; } .blockquote-reverse footer:after, blockquote.pull-right footer:after, .blockquote-reverse small:after, blockquote.pull-right small:after, .blockquote-reverse .small:after, blockquote.pull-right .small:after { content: '\00A0 \2014'; } address { margin-bottom: 20px; font-style: normal; line-height: 1.42857143; } code, kbd, pre, samp { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } code { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px; } kbd { padding: 2px 4px; font-size: 90%; color: #fff; background-color: #333; border-radius: 3px; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); } kbd kbd { padding: 0; font-size: 100%; font-weight: bold; -webkit-box-shadow: none; box-shadow: none; } pre { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 13px; line-height: 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; } pre code { padding: 0; font-size: inherit; color: inherit; white-space: pre-wrap; background-color: transparent; border-radius: 0; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 768px) { .container { width: 750px; } } @media (min-width: 992px) { .container { width: 970px; } } @media (min-width: 1200px) { .container { width: 1170px; } } .container-fluid { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } .row { margin-right: -15px; margin-left: -15px; } .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { position: relative; min-height: 1px; padding-right: 15px; padding-left: 15px; } .col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { float: left; } .col-xs-12 { width: 100%; } .col-xs-11 { width: 91.66666667%; } .col-xs-10 { width: 83.33333333%; } .col-xs-9 { width: 75%; } .col-xs-8 { width: 66.66666667%; } .col-xs-7 { width: 58.33333333%; } .col-xs-6 { width: 50%; } .col-xs-5 { width: 41.66666667%; } .col-xs-4 { width: 33.33333333%; } .col-xs-3 { width: 25%; } .col-xs-2 { width: 16.66666667%; } .col-xs-1 { width: 8.33333333%; } .col-xs-pull-12 { right: 100%; } .col-xs-pull-11 { right: 91.66666667%; } .col-xs-pull-10 { right: 83.33333333%; } .col-xs-pull-9 { right: 75%; } .col-xs-pull-8 { right: 66.66666667%; } .col-xs-pull-7 { right: 58.33333333%; } .col-xs-pull-6 { right: 50%; } .col-xs-pull-5 { right: 41.66666667%; } .col-xs-pull-4 { right: 33.33333333%; } .col-xs-pull-3 { right: 25%; } .col-xs-pull-2 { right: 16.66666667%; } .col-xs-pull-1 { right: 8.33333333%; } .col-xs-pull-0 { right: auto; } .col-xs-push-12 { left: 100%; } .col-xs-push-11 { left: 91.66666667%; } .col-xs-push-10 { left: 83.33333333%; } .col-xs-push-9 { left: 75%; } .col-xs-push-8 { left: 66.66666667%; } .col-xs-push-7 { left: 58.33333333%; } .col-xs-push-6 { left: 50%; } .col-xs-push-5 { left: 41.66666667%; } .col-xs-push-4 { left: 33.33333333%; } .col-xs-push-3 { left: 25%; } .col-xs-push-2 { left: 16.66666667%; } .col-xs-push-1 { left: 8.33333333%; } .col-xs-push-0 { left: auto; } .col-xs-offset-12 { margin-left: 100%; } .col-xs-offset-11 { margin-left: 91.66666667%; } .col-xs-offset-10 { margin-left: 83.33333333%; } .col-xs-offset-9 { margin-left: 75%; } .col-xs-offset-8 { margin-left: 66.66666667%; } .col-xs-offset-7 { margin-left: 58.33333333%; } .col-xs-offset-6 { margin-left: 50%; } .col-xs-offset-5 { margin-left: 41.66666667%; } .col-xs-offset-4 { margin-left: 33.33333333%; } .col-xs-offset-3 { margin-left: 25%; } .col-xs-offset-2 { margin-left: 16.66666667%; } .col-xs-offset-1 { margin-left: 8.33333333%; } .col-xs-offset-0 { margin-left: 0; } @media (min-width: 768px) { .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { float: left; } .col-sm-12 { width: 100%; } .col-sm-11 { width: 91.66666667%; } .col-sm-10 { width: 83.33333333%; } .col-sm-9 { width: 75%; } .col-sm-8 { width: 66.66666667%; } .col-sm-7 { width: 58.33333333%; } .col-sm-6 { width: 50%; } .col-sm-5 { width: 41.66666667%; } .col-sm-4 { width: 33.33333333%; } .col-sm-3 { width: 25%; } .col-sm-2 { width: 16.66666667%; } .col-sm-1 { width: 8.33333333%; } .col-sm-pull-12 { right: 100%; } .col-sm-pull-11 { right: 91.66666667%; } .col-sm-pull-10 { right: 83.33333333%; } .col-sm-pull-9 { right: 75%; } .col-sm-pull-8 { right: 66.66666667%; } .col-sm-pull-7 { right: 58.33333333%; } .col-sm-pull-6 { right: 50%; } .col-sm-pull-5 { right: 41.66666667%; } .col-sm-pull-4 { right: 33.33333333%; } .col-sm-pull-3 { right: 25%; } .col-sm-pull-2 { right: 16.66666667%; } .col-sm-pull-1 { right: 8.33333333%; } .col-sm-pull-0 { right: auto; } .col-sm-push-12 { left: 100%; } .col-sm-push-11 { left: 91.66666667%; } .col-sm-push-10 { left: 83.33333333%; } .col-sm-push-9 { left: 75%; } .col-sm-push-8 { left: 66.66666667%; } .col-sm-push-7 { left: 58.33333333%; } .col-sm-push-6 { left: 50%; } .col-sm-push-5 { left: 41.66666667%; } .col-sm-push-4 { left: 33.33333333%; } .col-sm-push-3 { left: 25%; } .col-sm-push-2 { left: 16.66666667%; } .col-sm-push-1 { left: 8.33333333%; } .col-sm-push-0 { left: auto; } .col-sm-offset-12 { margin-left: 100%; } .col-sm-offset-11 { margin-left: 91.66666667%; } .col-sm-offset-10 { margin-left: 83.33333333%; } .col-sm-offset-9 { margin-left: 75%; } .col-sm-offset-8 { margin-left: 66.66666667%; } .col-sm-offset-7 { margin-left: 58.33333333%; } .col-sm-offset-6 { margin-left: 50%; } .col-sm-offset-5 { margin-left: 41.66666667%; } .col-sm-offset-4 { margin-left: 33.33333333%; } .col-sm-offset-3 { margin-left: 25%; } .col-sm-offset-2 { margin-left: 16.66666667%; } .col-sm-offset-1 { margin-left: 8.33333333%; } .col-sm-offset-0 { margin-left: 0; } } @media (min-width: 992px) { .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { float: left; } .col-md-12 { width: 100%; } .col-md-11 { width: 91.66666667%; } .col-md-10 { width: 83.33333333%; } .col-md-9 { width: 75%; } .col-md-8 { width: 66.66666667%; } .col-md-7 { width: 58.33333333%; } .col-md-6 { width: 50%; } .col-md-5 { width: 41.66666667%; } .col-md-4 { width: 33.33333333%; } .col-md-3 { width: 25%; } .col-md-2 { width: 16.66666667%; } .col-md-1 { width: 8.33333333%; } .col-md-pull-12 { right: 100%; } .col-md-pull-11 { right: 91.66666667%; } .col-md-pull-10 { right: 83.33333333%; } .col-md-pull-9 { right: 75%; } .col-md-pull-8 { right: 66.66666667%; } .col-md-pull-7 { right: 58.33333333%; } .col-md-pull-6 { right: 50%; } .col-md-pull-5 { right: 41.66666667%; } .col-md-pull-4 { right: 33.33333333%; } .col-md-pull-3 { right: 25%; } .col-md-pull-2 { right: 16.66666667%; } .col-md-pull-1 { right: 8.33333333%; } .col-md-pull-0 { right: auto; } .col-md-push-12 { left: 100%; } .col-md-push-11 { left: 91.66666667%; } .col-md-push-10 { left: 83.33333333%; } .col-md-push-9 { left: 75%; } .col-md-push-8 { left: 66.66666667%; } .col-md-push-7 { left: 58.33333333%; } .col-md-push-6 { left: 50%; } .col-md-push-5 { left: 41.66666667%; } .col-md-push-4 { left: 33.33333333%; } .col-md-push-3 { left: 25%; } .col-md-push-2 { left: 16.66666667%; } .col-md-push-1 { left: 8.33333333%; } .col-md-push-0 { left: auto; } .col-md-offset-12 { margin-left: 100%; } .col-md-offset-11 { margin-left: 91.66666667%; } .col-md-offset-10 { margin-left: 83.33333333%; } .col-md-offset-9 { margin-left: 75%; } .col-md-offset-8 { margin-left: 66.66666667%; } .col-md-offset-7 { margin-left: 58.33333333%; } .col-md-offset-6 { margin-left: 50%; } .col-md-offset-5 { margin-left: 41.66666667%; } .col-md-offset-4 { margin-left: 33.33333333%; } .col-md-offset-3 { margin-left: 25%; } .col-md-offset-2 { margin-left: 16.66666667%; } .col-md-offset-1 { margin-left: 8.33333333%; } .col-md-offset-0 { margin-left: 0; } } @media (min-width: 1200px) { .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { float: left; } .col-lg-12 { width: 100%; } .col-lg-11 { width: 91.66666667%; } .col-lg-10 { width: 83.33333333%; } .col-lg-9 { width: 75%; } .col-lg-8 { width: 66.66666667%; } .col-lg-7 { width: 58.33333333%; } .col-lg-6 { width: 50%; } .col-lg-5 { width: 41.66666667%; } .col-lg-4 { width: 33.33333333%; } .col-lg-3 { width: 25%; } .col-lg-2 { width: 16.66666667%; } .col-lg-1 { width: 8.33333333%; } .col-lg-pull-12 { right: 100%; } .col-lg-pull-11 { right: 91.66666667%; } .col-lg-pull-10 { right: 83.33333333%; } .col-lg-pull-9 { right: 75%; } .col-lg-pull-8 { right: 66.66666667%; } .col-lg-pull-7 { right: 58.33333333%; } .col-lg-pull-6 { right: 50%; } .col-lg-pull-5 { right: 41.66666667%; } .col-lg-pull-4 { right: 33.33333333%; } .col-lg-pull-3 { right: 25%; } .col-lg-pull-2 { right: 16.66666667%; } .col-lg-pull-1 { right: 8.33333333%; } .col-lg-pull-0 { right: auto; } .col-lg-push-12 { left: 100%; } .col-lg-push-11 { left: 91.66666667%; } .col-lg-push-10 { left: 83.33333333%; } .col-lg-push-9 { left: 75%; } .col-lg-push-8 { left: 66.66666667%; } .col-lg-push-7 { left: 58.33333333%; } .col-lg-push-6 { left: 50%; } .col-lg-push-5 { left: 41.66666667%; } .col-lg-push-4 { left: 33.33333333%; } .col-lg-push-3 { left: 25%; } .col-lg-push-2 { left: 16.66666667%; } .col-lg-push-1 { left: 8.33333333%; } .col-lg-push-0 { left: auto; } .col-lg-offset-12 { margin-left: 100%; } .col-lg-offset-11 { margin-left: 91.66666667%; } .col-lg-offset-10 { margin-left: 83.33333333%; } .col-lg-offset-9 { margin-left: 75%; } .col-lg-offset-8 { margin-left: 66.66666667%; } .col-lg-offset-7 { margin-left: 58.33333333%; } .col-lg-offset-6 { margin-left: 50%; } .col-lg-offset-5 { margin-left: 41.66666667%; } .col-lg-offset-4 { margin-left: 33.33333333%; } .col-lg-offset-3 { margin-left: 25%; } .col-lg-offset-2 { margin-left: 16.66666667%; } .col-lg-offset-1 { margin-left: 8.33333333%; } .col-lg-offset-0 { margin-left: 0; } } table { background-color: transparent; } caption { padding-top: 8px; padding-bottom: 8px; color: #777; text-align: left; } th { text-align: left; } .table { width: 100%; max-width: 100%; margin-bottom: 20px; } .table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td { padding: 8px; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; } .table > thead > tr > th { vertical-align: bottom; border-bottom: 2px solid #ddd; } .table > caption + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > th, .table > thead:first-child > tr:first-child > th, .table > caption + thead > tr:first-child > td, .table > colgroup + thead > tr:first-child > td, .table > thead:first-child > tr:first-child > td { border-top: 0; } .table > tbody + tbody { border-top: 2px solid #ddd; } .table .table { background-color: #fff; } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 5px; } .table-bordered { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > tbody > tr > th, .table-bordered > tfoot > tr > th, .table-bordered > thead > tr > td, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > td { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > thead > tr > td { border-bottom-width: 2px; } .table-striped > tbody > tr:nth-of-type(odd) { background-color: #f9f9f9; } .table-hover > tbody > tr:hover { background-color: #f5f5f5; } table col[class*="col-"] { position: static; display: table-column; float: none; } table td[class*="col-"], table th[class*="col-"] { position: static; display: table-cell; float: none; } .table > thead > tr > td.active, .table > tbody > tr > td.active, .table > tfoot > tr > td.active, .table > thead > tr > th.active, .table > tbody > tr > th.active, .table > tfoot > tr > th.active, .table > thead > tr.active > td, .table > tbody > tr.active > td, .table > tfoot > tr.active > td, .table > thead > tr.active > th, .table > tbody > tr.active > th, .table > tfoot > tr.active > th { background-color: #f5f5f5; } .table-hover > tbody > tr > td.active:hover, .table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th { background-color: #e8e8e8; } .table > thead > tr > td.success, .table > tbody > tr > td.success, .table > tfoot > tr > td.success, .table > thead > tr > th.success, .table > tbody > tr > th.success, .table > tfoot > tr > th.success, .table > thead > tr.success > td, .table > tbody > tr.success > td, .table > tfoot > tr.success > td, .table > thead > tr.success > th, .table > tbody > tr.success > th, .table > tfoot > tr.success > th { background-color: #dff0d8; } .table-hover > tbody > tr > td.success:hover, .table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th { background-color: #d0e9c6; } .table > thead > tr > td.info, .table > tbody > tr > td.info, .table > tfoot > tr > td.info, .table > thead > tr > th.info, .table > tbody > tr > th.info, .table > tfoot > tr > th.info, .table > thead > tr.info > td, .table > tbody > tr.info > td, .table > tfoot > tr.info > td, .table > thead > tr.info > th, .table > tbody > tr.info > th, .table > tfoot > tr.info > th { background-color: #d9edf7; } .table-hover > tbody > tr > td.info:hover, .table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th { background-color: #c4e3f3; } .table > thead > tr > td.warning, .table > tbody > tr > td.warning, .table > tfoot > tr > td.warning, .table > thead > tr > th.warning, .table > tbody > tr > th.warning, .table > tfoot > tr > th.warning, .table > thead > tr.warning > td, .table > tbody > tr.warning > td, .table > tfoot > tr.warning > td, .table > thead > tr.warning > th, .table > tbody > tr.warning > th, .table > tfoot > tr.warning > th { background-color: #fcf8e3; } .table-hover > tbody > tr > td.warning:hover, .table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th { background-color: #faf2cc; } .table > thead > tr > td.danger, .table > tbody > tr > td.danger, .table > tfoot > tr > td.danger, .table > thead > tr > th.danger, .table > tbody > tr > th.danger, .table > tfoot > tr > th.danger, .table > thead > tr.danger > td, .table > tbody > tr.danger > td, .table > tfoot > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr.danger > th, .table > tfoot > tr.danger > th { background-color: #f2dede; } .table-hover > tbody > tr > td.danger:hover, .table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th { background-color: #ebcccc; } .table-responsive { min-height: .01%; overflow-x: auto; } @media screen and (max-width: 767px) { .table-responsive { width: 100%; margin-bottom: 15px; overflow-y: hidden; -ms-overflow-style: -ms-autohiding-scrollbar; border: 1px solid #ddd; } .table-responsive > .table { margin-bottom: 0; } .table-responsive > .table > thead > tr > th, .table-responsive > .table > tbody > tr > th, .table-responsive > .table > tfoot > tr > th, .table-responsive > .table > thead > tr > td, .table-responsive > .table > tbody > tr > td, .table-responsive > .table > tfoot > tr > td { white-space: nowrap; } .table-responsive > .table-bordered { border: 0; } .table-responsive > .table-bordered > thead > tr > th:first-child, .table-responsive > .table-bordered > tbody > tr > th:first-child, .table-responsive > .table-bordered > tfoot > tr > th:first-child, .table-responsive > .table-bordered > thead > tr > td:first-child, .table-responsive > .table-bordered > tbody > tr > td:first-child, .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .table-responsive > .table-bordered > thead > tr > th:last-child, .table-responsive > .table-bordered > tbody > tr > th:last-child, .table-responsive > .table-bordered > tfoot > tr > th:last-child, .table-responsive > .table-bordered > thead > tr > td:last-child, .table-responsive > .table-bordered > tbody > tr > td:last-child, .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .table-responsive > .table-bordered > tbody > tr:last-child > th, .table-responsive > .table-bordered > tfoot > tr:last-child > th, .table-responsive > .table-bordered > tbody > tr:last-child > td, .table-responsive > .table-bordered > tfoot > tr:last-child > td { border-bottom: 0; } } fieldset { min-width: 0; padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: 20px; font-size: 21px; line-height: inherit; color: #333; border: 0; border-bottom: 1px solid #e5e5e5; } label { display: inline-block; max-width: 100%; margin-bottom: 5px; font-weight: bold; } input[type="search"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; margin-top: 1px \9; line-height: normal; } input[type="file"] { display: block; } input[type="range"] { display: block; width: 100%; } select[multiple], select[size] { height: auto; } input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } output { display: block; padding-top: 7px; font-size: 14px; line-height: 1.42857143; color: #555; } .form-control { display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } .form-control:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); } .form-control::-moz-placeholder { color: #999; opacity: 1; } .form-control:-ms-input-placeholder { color: #999; } .form-control::-webkit-input-placeholder { color: #999; } .form-control::-ms-expand { background-color: transparent; border: 0; } .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { background-color: #eee; opacity: 1; } .form-control[disabled], fieldset[disabled] .form-control { cursor: not-allowed; } textarea.form-control { height: auto; } input[type="search"] { -webkit-appearance: none; } @media screen and (-webkit-min-device-pixel-ratio: 0) { input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { line-height: 34px; } input[type="date"].input-sm, input[type="time"].input-sm, input[type="datetime-local"].input-sm, input[type="month"].input-sm, .input-group-sm input[type="date"], .input-group-sm input[type="time"], .input-group-sm input[type="datetime-local"], .input-group-sm input[type="month"] { line-height: 30px; } input[type="date"].input-lg, input[type="time"].input-lg, input[type="datetime-local"].input-lg, input[type="month"].input-lg, .input-group-lg input[type="date"], .input-group-lg input[type="time"], .input-group-lg input[type="datetime-local"], .input-group-lg input[type="month"] { line-height: 46px; } } .form-group { margin-bottom: 15px; } .radio, .checkbox { position: relative; display: block; margin-top: 10px; margin-bottom: 10px; } .radio label, .checkbox label { min-height: 20px; padding-left: 20px; margin-bottom: 0; font-weight: normal; cursor: pointer; } .radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { position: absolute; margin-top: 4px \9; margin-left: -20px; } .radio + .radio, .checkbox + .checkbox { margin-top: -5px; } .radio-inline, .checkbox-inline { position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; font-weight: normal; vertical-align: middle; cursor: pointer; } .radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline { margin-top: 0; margin-left: 10px; } input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"].disabled, input[type="checkbox"].disabled, fieldset[disabled] input[type="radio"], fieldset[disabled] input[type="checkbox"] { cursor: not-allowed; } .radio-inline.disabled, .checkbox-inline.disabled, fieldset[disabled] .radio-inline, fieldset[disabled] .checkbox-inline { cursor: not-allowed; } .radio.disabled label, .checkbox.disabled label, fieldset[disabled] .radio label, fieldset[disabled] .checkbox label { cursor: not-allowed; } .form-control-static { min-height: 34px; padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; } .form-control-static.input-lg, .form-control-static.input-sm { padding-right: 0; padding-left: 0; } .input-sm { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-sm { height: 30px; line-height: 30px; } textarea.input-sm, select[multiple].input-sm { height: auto; } .form-group-sm .form-control { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .form-group-sm select.form-control { height: 30px; line-height: 30px; } .form-group-sm textarea.form-control, .form-group-sm select[multiple].form-control { height: auto; } .form-group-sm .form-control-static { height: 30px; min-height: 32px; padding: 6px 10px; font-size: 12px; line-height: 1.5; } .input-lg { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-lg { height: 46px; line-height: 46px; } textarea.input-lg, select[multiple].input-lg { height: auto; } .form-group-lg .form-control { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .form-group-lg select.form-control { height: 46px; line-height: 46px; } .form-group-lg textarea.form-control, .form-group-lg select[multiple].form-control { height: auto; } .form-group-lg .form-control-static { height: 46px; min-height: 38px; padding: 11px 16px; font-size: 18px; line-height: 1.3333333; } .has-feedback { position: relative; } .has-feedback .form-control { padding-right: 42.5px; } .form-control-feedback { position: absolute; top: 0; right: 0; z-index: 2; display: block; width: 34px; height: 34px; line-height: 34px; text-align: center; pointer-events: none; } .input-lg + .form-control-feedback, .input-group-lg + .form-control-feedback, .form-group-lg .form-control + .form-control-feedback { width: 46px; height: 46px; line-height: 46px; } .input-sm + .form-control-feedback, .input-group-sm + .form-control-feedback, .form-group-sm .form-control + .form-control-feedback { width: 30px; height: 30px; line-height: 30px; } .has-success .help-block, .has-success .control-label, .has-success .radio, .has-success .checkbox, .has-success .radio-inline, .has-success .checkbox-inline, .has-success.radio label, .has-success.checkbox label, .has-success.radio-inline label, .has-success.checkbox-inline label { color: #3c763d; } .has-success .form-control { border-color: #3c763d; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-success .form-control:focus { border-color: #2b542c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; } .has-success .input-group-addon { color: #3c763d; background-color: #dff0d8; border-color: #3c763d; } .has-success .form-control-feedback { color: #3c763d; } .has-warning .help-block, .has-warning .control-label, .has-warning .radio, .has-warning .checkbox, .has-warning .radio-inline, .has-warning .checkbox-inline, .has-warning.radio label, .has-warning.checkbox label, .has-warning.radio-inline label, .has-warning.checkbox-inline label { color: #8a6d3b; } .has-warning .form-control { border-color: #8a6d3b; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-warning .form-control:focus { border-color: #66512c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; } .has-warning .input-group-addon { color: #8a6d3b; background-color: #fcf8e3; border-color: #8a6d3b; } .has-warning .form-control-feedback { color: #8a6d3b; } .has-error .help-block, .has-error .control-label, .has-error .radio, .has-error .checkbox, .has-error .radio-inline, .has-error .checkbox-inline, .has-error.radio label, .has-error.checkbox label, .has-error.radio-inline label, .has-error.checkbox-inline label { color: #a94442; } .has-error .form-control { border-color: #a94442; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-error .form-control:focus { border-color: #843534; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } .has-error .input-group-addon { color: #a94442; background-color: #f2dede; border-color: #a94442; } .has-error .form-control-feedback { color: #a94442; } .has-feedback label ~ .form-control-feedback { top: 25px; } .has-feedback label.sr-only ~ .form-control-feedback { top: 0; } .help-block { display: block; margin-top: 5px; margin-bottom: 10px; color: #737373; } @media (min-width: 768px) { .form-inline .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .form-inline .form-control { display: inline-block; width: auto; vertical-align: middle; } .form-inline .form-control-static { display: inline-block; } .form-inline .input-group { display: inline-table; vertical-align: middle; } .form-inline .input-group .input-group-addon, .form-inline .input-group .input-group-btn, .form-inline .input-group .form-control { width: auto; } .form-inline .input-group > .form-control { width: 100%; } .form-inline .control-label { margin-bottom: 0; vertical-align: middle; } .form-inline .radio, .form-inline .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .form-inline .radio label, .form-inline .checkbox label { padding-left: 0; } .form-inline .radio input[type="radio"], .form-inline .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .form-inline .has-feedback .form-control-feedback { top: 0; } } .form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline { padding-top: 7px; margin-top: 0; margin-bottom: 0; } .form-horizontal .radio, .form-horizontal .checkbox { min-height: 27px; } .form-horizontal .form-group { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .form-horizontal .control-label { padding-top: 7px; margin-bottom: 0; text-align: right; } } .form-horizontal .has-feedback .form-control-feedback { right: 15px; } @media (min-width: 768px) { .form-horizontal .form-group-lg .control-label { padding-top: 11px; font-size: 18px; } } @media (min-width: 768px) { .form-horizontal .form-group-sm .control-label { padding-top: 6px; font-size: 12px; } } .btn { display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: normal; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-image: none; border: 1px solid transparent; border-radius: 4px; } .btn:focus, .btn:active:focus, .btn.active:focus, .btn.focus, .btn:active.focus, .btn.active.focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .btn:hover, .btn:focus, .btn.focus { color: #333; text-decoration: none; } .btn:active, .btn.active { background-image: none; outline: 0; -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn.disabled, .btn[disabled], fieldset[disabled] .btn { cursor: not-allowed; filter: alpha(opacity=65); -webkit-box-shadow: none; box-shadow: none; opacity: .65; } a.btn.disabled, fieldset[disabled] a.btn { pointer-events: none; } .btn-default { color: #333; background-color: #fff; border-color: #ccc; } .btn-default:focus, .btn-default.focus { color: #333; background-color: #e6e6e6; border-color: #8c8c8c; } .btn-default:hover { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus { color: #333; background-color: #d4d4d4; border-color: #8c8c8c; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { background-image: none; } .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus { background-color: #fff; border-color: #ccc; } .btn-default .badge { color: #fff; background-color: #333; } .btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; } .btn-primary:focus, .btn-primary.focus { color: #fff; background-color: #286090; border-color: #122b40; } .btn-primary:hover { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active:hover, .btn-primary.active:hover, .open > .dropdown-toggle.btn-primary:hover, .btn-primary:active:focus, .btn-primary.active:focus, .open > .dropdown-toggle.btn-primary:focus, .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus { color: #fff; background-color: #204d74; border-color: #122b40; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { background-image: none; } .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled.focus, .btn-primary[disabled].focus, fieldset[disabled] .btn-primary.focus { background-color: #337ab7; border-color: #2e6da4; } .btn-primary .badge { color: #337ab7; background-color: #fff; } .btn-success { color: #fff; background-color: #5cb85c; border-color: #4cae4c; } .btn-success:focus, .btn-success.focus { color: #fff; background-color: #449d44; border-color: #255625; } .btn-success:hover { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active:hover, .btn-success.active:hover, .open > .dropdown-toggle.btn-success:hover, .btn-success:active:focus, .btn-success.active:focus, .open > .dropdown-toggle.btn-success:focus, .btn-success:active.focus, .btn-success.active.focus, .open > .dropdown-toggle.btn-success.focus { color: #fff; background-color: #398439; border-color: #255625; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { background-image: none; } .btn-success.disabled:hover, .btn-success[disabled]:hover, fieldset[disabled] .btn-success:hover, .btn-success.disabled:focus, .btn-success[disabled]:focus, fieldset[disabled] .btn-success:focus, .btn-success.disabled.focus, .btn-success[disabled].focus, fieldset[disabled] .btn-success.focus { background-color: #5cb85c; border-color: #4cae4c; } .btn-success .badge { color: #5cb85c; background-color: #fff; } .btn-info { color: #fff; background-color: #5bc0de; border-color: #46b8da; } .btn-info:focus, .btn-info.focus { color: #fff; background-color: #31b0d5; border-color: #1b6d85; } .btn-info:hover { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active:hover, .btn-info.active:hover, .open > .dropdown-toggle.btn-info:hover, .btn-info:active:focus, .btn-info.active:focus, .open > .dropdown-toggle.btn-info:focus, .btn-info:active.focus, .btn-info.active.focus, .open > .dropdown-toggle.btn-info.focus { color: #fff; background-color: #269abc; border-color: #1b6d85; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { background-image: none; } .btn-info.disabled:hover, .btn-info[disabled]:hover, fieldset[disabled] .btn-info:hover, .btn-info.disabled:focus, .btn-info[disabled]:focus, fieldset[disabled] .btn-info:focus, .btn-info.disabled.focus, .btn-info[disabled].focus, fieldset[disabled] .btn-info.focus { background-color: #5bc0de; border-color: #46b8da; } .btn-info .badge { color: #5bc0de; background-color: #fff; } .btn-warning { color: #fff; background-color: #f0ad4e; border-color: #eea236; } .btn-warning:focus, .btn-warning.focus { color: #fff; background-color: #ec971f; border-color: #985f0d; } .btn-warning:hover { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active:hover, .btn-warning.active:hover, .open > .dropdown-toggle.btn-warning:hover, .btn-warning:active:focus, .btn-warning.active:focus, .open > .dropdown-toggle.btn-warning:focus, .btn-warning:active.focus, .btn-warning.active.focus, .open > .dropdown-toggle.btn-warning.focus { color: #fff; background-color: #d58512; border-color: #985f0d; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { background-image: none; } .btn-warning.disabled:hover, .btn-warning[disabled]:hover, fieldset[disabled] .btn-warning:hover, .btn-warning.disabled:focus, .btn-warning[disabled]:focus, fieldset[disabled] .btn-warning:focus, .btn-warning.disabled.focus, .btn-warning[disabled].focus, fieldset[disabled] .btn-warning.focus { background-color: #f0ad4e; border-color: #eea236; } .btn-warning .badge { color: #f0ad4e; background-color: #fff; } .btn-danger { color: #fff; background-color: #d9534f; border-color: #d43f3a; } .btn-danger:focus, .btn-danger.focus { color: #fff; background-color: #c9302c; border-color: #761c19; } .btn-danger:hover { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active:hover, .btn-danger.active:hover, .open > .dropdown-toggle.btn-danger:hover, .btn-danger:active:focus, .btn-danger.active:focus, .open > .dropdown-toggle.btn-danger:focus, .btn-danger:active.focus, .btn-danger.active.focus, .open > .dropdown-toggle.btn-danger.focus { color: #fff; background-color: #ac2925; border-color: #761c19; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { background-image: none; } .btn-danger.disabled:hover, .btn-danger[disabled]:hover, fieldset[disabled] .btn-danger:hover, .btn-danger.disabled:focus, .btn-danger[disabled]:focus, fieldset[disabled] .btn-danger:focus, .btn-danger.disabled.focus, .btn-danger[disabled].focus, fieldset[disabled] .btn-danger.focus { background-color: #d9534f; border-color: #d43f3a; } .btn-danger .badge { color: #d9534f; background-color: #fff; } .btn-link { font-weight: normal; color: #337ab7; border-radius: 0; } .btn-link, .btn-link:active, .btn-link.active, .btn-link[disabled], fieldset[disabled] .btn-link { background-color: transparent; -webkit-box-shadow: none; box-shadow: none; } .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active { border-color: transparent; } .btn-link:hover, .btn-link:focus { color: #23527c; text-decoration: underline; background-color: transparent; } .btn-link[disabled]:hover, fieldset[disabled] .btn-link:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:focus { color: #777; text-decoration: none; } .btn-lg, .btn-group-lg > .btn { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .btn-sm, .btn-group-sm > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-xs, .btn-group-xs > .btn { padding: 1px 5px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-block { display: block; width: 100%; } .btn-block + .btn-block { margin-top: 5px; } input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } .fade { opacity: 0; -webkit-transition: opacity .15s linear; -o-transition: opacity .15s linear; transition: opacity .15s linear; } .fade.in { opacity: 1; } .collapse { display: none; } .collapse.in { display: block; } tr.collapse.in { display: table-row; } tbody.collapse.in { display: table-row-group; } .collapsing { position: relative; height: 0; overflow: hidden; -webkit-transition-timing-function: ease; -o-transition-timing-function: ease; transition-timing-function: ease; -webkit-transition-duration: .35s; -o-transition-duration: .35s; transition-duration: .35s; -webkit-transition-property: height, visibility; -o-transition-property: height, visibility; transition-property: height, visibility; } .caret { display: inline-block; width: 0; height: 0; margin-left: 2px; vertical-align: middle; border-top: 4px dashed; border-top: 4px solid \9; border-right: 4px solid transparent; border-left: 4px solid transparent; } .dropup, .dropdown { position: relative; } .dropdown-toggle:focus { outline: 0; } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 160px; padding: 5px 0; margin: 2px 0 0; font-size: 14px; text-align: left; list-style: none; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .15); border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175); } .dropdown-menu.pull-right { right: 0; left: auto; } .dropdown-menu .divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .dropdown-menu > li > a { display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.42857143; color: #333; white-space: nowrap; } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { color: #262626; text-decoration: none; background-color: #f5f5f5; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { color: #fff; text-decoration: none; background-color: #337ab7; outline: 0; } .dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { color: #777; } .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { text-decoration: none; cursor: not-allowed; background-color: transparent; background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .open > .dropdown-menu { display: block; } .open > a { outline: 0; } .dropdown-menu-right { right: 0; left: auto; } .dropdown-menu-left { right: auto; left: 0; } .dropdown-header { display: block; padding: 3px 20px; font-size: 12px; line-height: 1.42857143; color: #777; white-space: nowrap; } .dropdown-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 990; } .pull-right > .dropdown-menu { right: 0; left: auto; } .dropup .caret, .navbar-fixed-bottom .dropdown .caret { content: ""; border-top: 0; border-bottom: 4px dashed; border-bottom: 4px solid \9; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 2px; } @media (min-width: 768px) { .navbar-right .dropdown-menu { right: 0; left: auto; } .navbar-right .dropdown-menu-left { right: auto; left: 0; } } .btn-group, .btn-group-vertical { position: relative; display: inline-block; vertical-align: middle; } .btn-group > .btn, .btn-group-vertical > .btn { position: relative; float: left; } .btn-group > .btn:hover, .btn-group-vertical > .btn:hover, .btn-group > .btn:focus, .btn-group-vertical > .btn:focus, .btn-group > .btn:active, .btn-group-vertical > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn.active { z-index: 2; } .btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group { margin-left: -1px; } .btn-toolbar { margin-left: -5px; } .btn-toolbar .btn, .btn-toolbar .btn-group, .btn-toolbar .input-group { float: left; } .btn-toolbar > .btn, .btn-toolbar > .btn-group, .btn-toolbar > .input-group { margin-left: 5px; } .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { border-radius: 0; } .btn-group > .btn:first-child { margin-left: 0; } .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group > .btn-group { float: left; } .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } .btn-group > .btn + .dropdown-toggle { padding-right: 8px; padding-left: 8px; } .btn-group > .btn-lg + .dropdown-toggle { padding-right: 12px; padding-left: 12px; } .btn-group.open .dropdown-toggle { -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn-group.open .dropdown-toggle.btn-link { -webkit-box-shadow: none; box-shadow: none; } .btn .caret { margin-left: 0; } .btn-lg .caret { border-width: 5px 5px 0; border-bottom-width: 0; } .dropup .btn-lg .caret { border-width: 0 5px 5px; } .btn-group-vertical > .btn, .btn-group-vertical > .btn-group, .btn-group-vertical > .btn-group > .btn { display: block; float: none; width: 100%; max-width: 100%; } .btn-group-vertical > .btn-group > .btn { float: none; } .btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group { margin-top: -1px; margin-left: 0; } .btn-group-vertical > .btn:not(:first-child):not(:last-child) { border-radius: 0; } .btn-group-vertical > .btn:first-child:not(:last-child) { border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn:last-child:not(:first-child) { border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .btn-group-justified { display: table; width: 100%; table-layout: fixed; border-collapse: separate; } .btn-group-justified > .btn, .btn-group-justified > .btn-group { display: table-cell; float: none; width: 1%; } .btn-group-justified > .btn-group .btn { width: 100%; } .btn-group-justified > .btn-group .dropdown-menu { left: auto; } [data-toggle="buttons"] > .btn input[type="radio"], [data-toggle="buttons"] > .btn-group > .btn input[type="radio"], [data-toggle="buttons"] > .btn input[type="checkbox"], [data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { position: absolute; clip: rect(0, 0, 0, 0); pointer-events: none; } .input-group { position: relative; display: table; border-collapse: separate; } .input-group[class*="col-"] { float: none; padding-right: 0; padding-left: 0; } .input-group .form-control { position: relative; z-index: 2; float: left; width: 100%; margin-bottom: 0; } .input-group .form-control:focus { z-index: 3; } .input-group-lg > .form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .btn { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-group-lg > .form-control, select.input-group-lg > .input-group-addon, select.input-group-lg > .input-group-btn > .btn { height: 46px; line-height: 46px; } textarea.input-group-lg > .form-control, textarea.input-group-lg > .input-group-addon, textarea.input-group-lg > .input-group-btn > .btn, select[multiple].input-group-lg > .form-control, select[multiple].input-group-lg > .input-group-addon, select[multiple].input-group-lg > .input-group-btn > .btn { height: auto; } .input-group-sm > .form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .btn { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-group-sm > .form-control, select.input-group-sm > .input-group-addon, select.input-group-sm > .input-group-btn > .btn { height: 30px; line-height: 30px; } textarea.input-group-sm > .form-control, textarea.input-group-sm > .input-group-addon, textarea.input-group-sm > .input-group-btn > .btn, select[multiple].input-group-sm > .form-control, select[multiple].input-group-sm > .input-group-addon, select[multiple].input-group-sm > .input-group-btn > .btn { height: auto; } .input-group-addon, .input-group-btn, .input-group .form-control { display: table-cell; } .input-group-addon:not(:first-child):not(:last-child), .input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) { border-radius: 0; } .input-group-addon, .input-group-btn { width: 1%; white-space: nowrap; vertical-align: middle; } .input-group-addon { padding: 6px 12px; font-size: 14px; font-weight: normal; line-height: 1; color: #555; text-align: center; background-color: #eee; border: 1px solid #ccc; border-radius: 4px; } .input-group-addon.input-sm { padding: 5px 10px; font-size: 12px; border-radius: 3px; } .input-group-addon.input-lg { padding: 10px 16px; font-size: 18px; border-radius: 6px; } .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] { margin-top: 0; } .input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group-addon:first-child { border-right: 0; } .input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, .input-group-btn:first-child > .btn:not(:first-child), .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group-addon:last-child { border-left: 0; } .input-group-btn { position: relative; font-size: 0; white-space: nowrap; } .input-group-btn > .btn { position: relative; } .input-group-btn > .btn + .btn { margin-left: -1px; } .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { z-index: 2; } .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group { margin-right: -1px; } .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group { z-index: 2; margin-left: -1px; } .nav { padding-left: 0; margin-bottom: 0; list-style: none; } .nav > li { position: relative; display: block; } .nav > li > a { position: relative; display: block; padding: 10px 15px; } .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; background-color: #eee; } .nav > li.disabled > a { color: #777; } .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { color: #777; text-decoration: none; cursor: not-allowed; background-color: transparent; } .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { background-color: #eee; border-color: #337ab7; } .nav .nav-divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .nav > li > a > img { max-width: none; } .nav-tabs { border-bottom: 1px solid #ddd; } .nav-tabs > li { float: left; margin-bottom: -1px; } .nav-tabs > li > a { margin-right: 2px; line-height: 1.42857143; border: 1px solid transparent; border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eee #eee #ddd; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #555; cursor: default; background-color: #fff; border: 1px solid #ddd; border-bottom-color: transparent; } .nav-tabs.nav-justified { width: 100%; border-bottom: 0; } .nav-tabs.nav-justified > li { float: none; } .nav-tabs.nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-tabs.nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-tabs.nav-justified > li { display: table-cell; width: 1%; } .nav-tabs.nav-justified > li > a { margin-bottom: 0; } } .nav-tabs.nav-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border-bottom-color: #fff; } } .nav-pills > li { float: left; } .nav-pills > li > a { border-radius: 4px; } .nav-pills > li + li { margin-left: 2px; } .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { color: #fff; background-color: #337ab7; } .nav-stacked > li { float: none; } .nav-stacked > li + li { margin-top: 2px; margin-left: 0; } .nav-justified { width: 100%; } .nav-justified > li { float: none; } .nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-justified > li { display: table-cell; width: 1%; } .nav-justified > li > a { margin-bottom: 0; } } .nav-tabs-justified { border-bottom: 0; } .nav-tabs-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border-bottom-color: #fff; } } .tab-content > .tab-pane { display: none; } .tab-content > .active { display: block; } .nav-tabs .dropdown-menu { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar { position: relative; min-height: 50px; margin-bottom: 20px; border: 1px solid transparent; } @media (min-width: 768px) { .navbar { border-radius: 4px; } } @media (min-width: 768px) { .navbar-header { float: left; } } .navbar-collapse { padding-right: 15px; padding-left: 15px; overflow-x: visible; -webkit-overflow-scrolling: touch; border-top: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); } .navbar-collapse.in { overflow-y: auto; } @media (min-width: 768px) { .navbar-collapse { width: auto; border-top: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-collapse.collapse { display: block !important; height: auto !important; padding-bottom: 0; overflow: visible !important; } .navbar-collapse.in { overflow-y: visible; } .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { padding-right: 0; padding-left: 0; } } .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 340px; } @media (max-device-width: 480px) and (orientation: landscape) { .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 200px; } } .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: 0; margin-left: 0; } } .navbar-static-top { z-index: 1000; border-width: 0 0 1px; } @media (min-width: 768px) { .navbar-static-top { border-radius: 0; } } .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; z-index: 1030; } @media (min-width: 768px) { .navbar-fixed-top, .navbar-fixed-bottom { border-radius: 0; } } .navbar-fixed-top { top: 0; border-width: 0 0 1px; } .navbar-fixed-bottom { bottom: 0; margin-bottom: 0; border-width: 1px 0 0; } .navbar-brand { float: left; height: 50px; padding: 15px 15px; font-size: 18px; line-height: 20px; } .navbar-brand:hover, .navbar-brand:focus { text-decoration: none; } .navbar-brand > img { display: block; } @media (min-width: 768px) { .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { margin-left: -15px; } } .navbar-toggle { position: relative; float: right; padding: 9px 10px; margin-top: 8px; margin-right: 15px; margin-bottom: 8px; background-color: transparent; background-image: none; border: 1px solid transparent; border-radius: 4px; } .navbar-toggle:focus { outline: 0; } .navbar-toggle .icon-bar { display: block; width: 22px; height: 2px; border-radius: 1px; } .navbar-toggle .icon-bar + .icon-bar { margin-top: 4px; } @media (min-width: 768px) { .navbar-toggle { display: none; } } .navbar-nav { margin: 7.5px -15px; } .navbar-nav > li > a { padding-top: 10px; padding-bottom: 10px; line-height: 20px; } @media (max-width: 767px) { .navbar-nav .open .dropdown-menu { position: static; float: none; width: auto; margin-top: 0; background-color: transparent; border: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header { padding: 5px 15px 5px 25px; } .navbar-nav .open .dropdown-menu > li > a { line-height: 20px; } .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus { background-image: none; } } @media (min-width: 768px) { .navbar-nav { float: left; margin: 0; } .navbar-nav > li { float: left; } .navbar-nav > li > a { padding-top: 15px; padding-bottom: 15px; } } .navbar-form { padding: 10px 15px; margin-top: 8px; margin-right: -15px; margin-bottom: 8px; margin-left: -15px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); } @media (min-width: 768px) { .navbar-form .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .navbar-form .form-control { display: inline-block; width: auto; vertical-align: middle; } .navbar-form .form-control-static { display: inline-block; } .navbar-form .input-group { display: inline-table; vertical-align: middle; } .navbar-form .input-group .input-group-addon, .navbar-form .input-group .input-group-btn, .navbar-form .input-group .form-control { width: auto; } .navbar-form .input-group > .form-control { width: 100%; } .navbar-form .control-label { margin-bottom: 0; vertical-align: middle; } .navbar-form .radio, .navbar-form .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .navbar-form .radio label, .navbar-form .checkbox label { padding-left: 0; } .navbar-form .radio input[type="radio"], .navbar-form .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .navbar-form .has-feedback .form-control-feedback { top: 0; } } @media (max-width: 767px) { .navbar-form .form-group { margin-bottom: 5px; } .navbar-form .form-group:last-child { margin-bottom: 0; } } @media (min-width: 768px) { .navbar-form { width: auto; padding-top: 0; padding-bottom: 0; margin-right: 0; margin-left: 0; border: 0; -webkit-box-shadow: none; box-shadow: none; } } .navbar-nav > li > .dropdown-menu { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { margin-bottom: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .navbar-btn { margin-top: 8px; margin-bottom: 8px; } .navbar-btn.btn-sm { margin-top: 10px; margin-bottom: 10px; } .navbar-btn.btn-xs { margin-top: 14px; margin-bottom: 14px; } .navbar-text { margin-top: 15px; margin-bottom: 15px; } @media (min-width: 768px) { .navbar-text { float: left; margin-right: 15px; margin-left: 15px; } } @media (min-width: 768px) { .navbar-left { float: left !important; } .navbar-right { float: right !important; margin-right: -15px; } .navbar-right ~ .navbar-right { margin-right: 0; } } .navbar-default { background-color: #f8f8f8; border-color: #e7e7e7; } .navbar-default .navbar-brand { color: #777; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { color: #5e5e5e; background-color: transparent; } .navbar-default .navbar-text { color: #777; } .navbar-default .navbar-nav > li > a { color: #777; } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { color: #ccc; background-color: transparent; } .navbar-default .navbar-toggle { border-color: #ddd; } .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { background-color: #ddd; } .navbar-default .navbar-toggle .icon-bar { background-color: #888; } .navbar-default .navbar-collapse, .navbar-default .navbar-form { border-color: #e7e7e7; } .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { color: #555; background-color: #e7e7e7; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { color: #777; } .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #ccc; background-color: transparent; } } .navbar-default .navbar-link { color: #777; } .navbar-default .navbar-link:hover { color: #333; } .navbar-default .btn-link { color: #777; } .navbar-default .btn-link:hover, .navbar-default .btn-link:focus { color: #333; } .navbar-default .btn-link[disabled]:hover, fieldset[disabled] .navbar-default .btn-link:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:focus { color: #ccc; } .navbar-inverse { background-color: #222; border-color: #080808; } .navbar-inverse .navbar-brand { color: #9d9d9d; } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-text { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { color: #444; background-color: transparent; } .navbar-inverse .navbar-toggle { border-color: #333; } .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { background-color: #333; } .navbar-inverse .navbar-toggle .icon-bar { background-color: #fff; } .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { border-color: #101010; } .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { color: #fff; background-color: #080808; } @media (max-width: 767px) { .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { border-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu .divider { background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #444; background-color: transparent; } } .navbar-inverse .navbar-link { color: #9d9d9d; } .navbar-inverse .navbar-link:hover { color: #fff; } .navbar-inverse .btn-link { color: #9d9d9d; } .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { color: #fff; } .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { color: #444; } .breadcrumb { padding: 8px 15px; margin-bottom: 20px; list-style: none; background-color: #f5f5f5; border-radius: 4px; } .breadcrumb > li { display: inline-block; } .breadcrumb > li + li:before { padding: 0 5px; color: #ccc; content: "/\00a0"; } .breadcrumb > .active { color: #777; } .pagination { display: inline-block; padding-left: 0; margin: 20px 0; border-radius: 4px; } .pagination > li { display: inline; } .pagination > li > a, .pagination > li > span { position: relative; float: left; padding: 6px 12px; margin-left: -1px; line-height: 1.42857143; color: #337ab7; text-decoration: none; background-color: #fff; border: 1px solid #ddd; } .pagination > li:first-child > a, .pagination > li:first-child > span { margin-left: 0; border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .pagination > li:last-child > a, .pagination > li:last-child > span { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .pagination > li > a:hover, .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { z-index: 2; color: #23527c; background-color: #eee; border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, .pagination > .active > a:hover, .pagination > .active > span:hover, .pagination > .active > a:focus, .pagination > .active > span:focus { z-index: 3; color: #fff; cursor: default; background-color: #337ab7; border-color: #337ab7; } .pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { color: #777; cursor: not-allowed; background-color: #fff; border-color: #ddd; } .pagination-lg > li > a, .pagination-lg > li > span { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; } .pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span { border-top-left-radius: 6px; border-bottom-left-radius: 6px; } .pagination-lg > li:last-child > a, .pagination-lg > li:last-child > span { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } .pagination-sm > li > a, .pagination-sm > li > span { padding: 5px 10px; font-size: 12px; line-height: 1.5; } .pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span { border-top-left-radius: 3px; border-bottom-left-radius: 3px; } .pagination-sm > li:last-child > a, .pagination-sm > li:last-child > span { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } .pager { padding-left: 0; margin: 20px 0; text-align: center; list-style: none; } .pager li { display: inline; } .pager li > a, .pager li > span { display: inline-block; padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; border-radius: 15px; } .pager li > a:hover, .pager li > a:focus { text-decoration: none; background-color: #eee; } .pager .next > a, .pager .next > span { float: right; } .pager .previous > a, .pager .previous > span { float: left; } .pager .disabled > a, .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span { color: #777; cursor: not-allowed; background-color: #fff; } .label { display: inline; padding: .2em .6em .3em; font-size: 75%; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em; } a.label:hover, a.label:focus { color: #fff; text-decoration: none; cursor: pointer; } .label:empty { display: none; } .btn .label { position: relative; top: -1px; } .label-default { background-color: #777; } .label-default[href]:hover, .label-default[href]:focus { background-color: #5e5e5e; } .label-primary { background-color: #337ab7; } .label-primary[href]:hover, .label-primary[href]:focus { background-color: #286090; } .label-success { background-color: #5cb85c; } .label-success[href]:hover, .label-success[href]:focus { background-color: #449d44; } .label-info { background-color: #5bc0de; } .label-info[href]:hover, .label-info[href]:focus { background-color: #31b0d5; } .label-warning { background-color: #f0ad4e; } .label-warning[href]:hover, .label-warning[href]:focus { background-color: #ec971f; } .label-danger { background-color: #d9534f; } .label-danger[href]:hover, .label-danger[href]:focus { background-color: #c9302c; } .badge { display: inline-block; min-width: 10px; padding: 3px 7px; font-size: 12px; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: middle; background-color: #777; border-radius: 10px; } .badge:empty { display: none; } .btn .badge { position: relative; top: -1px; } .btn-xs .badge, .btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } a.badge:hover, a.badge:focus { color: #fff; text-decoration: none; cursor: pointer; } .list-group-item.active > .badge, .nav-pills > .active > a > .badge { color: #337ab7; background-color: #fff; } .list-group-item > .badge { float: right; } .list-group-item > .badge + .badge { margin-right: 5px; } .nav-pills > li > a > .badge { margin-left: 3px; } .jumbotron { padding-top: 30px; padding-bottom: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; } .jumbotron h1, .jumbotron .h1 { color: inherit; } .jumbotron p { margin-bottom: 15px; font-size: 21px; font-weight: 200; } .jumbotron > hr { border-top-color: #d5d5d5; } .container .jumbotron, .container-fluid .jumbotron { padding-right: 15px; padding-left: 15px; border-radius: 6px; } .jumbotron .container { max-width: 100%; } @media screen and (min-width: 768px) { .jumbotron { padding-top: 48px; padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { padding-right: 60px; padding-left: 60px; } .jumbotron h1, .jumbotron .h1 { font-size: 63px; } } .thumbnail { display: block; padding: 4px; margin-bottom: 20px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: border .2s ease-in-out; -o-transition: border .2s ease-in-out; transition: border .2s ease-in-out; } .thumbnail > img, .thumbnail a > img { margin-right: auto; margin-left: auto; } a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { border-color: #337ab7; } .thumbnail .caption { padding: 9px; color: #333; } .alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; } .alert h4 { margin-top: 0; color: inherit; } .alert .alert-link { font-weight: bold; } .alert > p, .alert > ul { margin-bottom: 0; } .alert > p + p { margin-top: 5px; } .alert-dismissable, .alert-dismissible { padding-right: 35px; } .alert-dismissable .close, .alert-dismissible .close { position: relative; top: -2px; right: -21px; color: inherit; } .alert-success { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .alert-success hr { border-top-color: #c9e2b3; } .alert-success .alert-link { color: #2b542c; } .alert-info { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .alert-info hr { border-top-color: #a6e1ec; } .alert-info .alert-link { color: #245269; } .alert-warning { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .alert-warning hr { border-top-color: #f7e1b5; } .alert-warning .alert-link { color: #66512c; } .alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .alert-danger hr { border-top-color: #e4b9c0; } .alert-danger .alert-link { color: #843534; } @-webkit-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-o-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } .progress { height: 20px; margin-bottom: 20px; overflow: hidden; background-color: #f5f5f5; border-radius: 4px; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); } .progress-bar { float: left; width: 0; height: 100%; font-size: 12px; line-height: 20px; color: #fff; text-align: center; background-color: #337ab7; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); -webkit-transition: width .6s ease; -o-transition: width .6s ease; transition: width .6s ease; } .progress-striped .progress-bar, .progress-bar-striped { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -webkit-background-size: 40px 40px; background-size: 40px 40px; } .progress.active .progress-bar, .progress-bar.active { -webkit-animation: progress-bar-stripes 2s linear infinite; -o-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { background-color: #5cb85c; } .progress-striped .progress-bar-success { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-info { background-color: #5bc0de; } .progress-striped .progress-bar-info { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-warning { background-color: #f0ad4e; } .progress-striped .progress-bar-warning { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .progress-bar-danger { background-color: #d9534f; } .progress-striped .progress-bar-danger { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); } .media { margin-top: 15px; } .media:first-child { margin-top: 0; } .media, .media-body { overflow: hidden; zoom: 1; } .media-body { width: 10000px; } .media-object { display: block; } .media-object.img-thumbnail { max-width: none; } .media-right, .media > .pull-right { padding-left: 10px; } .media-left, .media > .pull-left { padding-right: 10px; } .media-left, .media-right, .media-body { display: table-cell; vertical-align: top; } .media-middle { vertical-align: middle; } .media-bottom { vertical-align: bottom; } .media-heading { margin-top: 0; margin-bottom: 5px; } .media-list { padding-left: 0; list-style: none; } .list-group { padding-left: 0; margin-bottom: 20px; } .list-group-item { position: relative; display: block; padding: 10px 15px; margin-bottom: -1px; background-color: #fff; border: 1px solid #ddd; } .list-group-item:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } .list-group-item:last-child { margin-bottom: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } a.list-group-item, button.list-group-item { color: #555; } a.list-group-item .list-group-item-heading, button.list-group-item .list-group-item-heading { color: #333; } a.list-group-item:hover, button.list-group-item:hover, a.list-group-item:focus, button.list-group-item:focus { color: #555; text-decoration: none; background-color: #f5f5f5; } button.list-group-item { width: 100%; text-align: left; } .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { color: #777; cursor: not-allowed; background-color: #eee; } .list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading { color: inherit; } .list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text { color: #777; } .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { z-index: 2; color: #fff; background-color: #337ab7; border-color: #337ab7; } .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, .list-group-item.active:focus .list-group-item-heading, .list-group-item.active .list-group-item-heading > small, .list-group-item.active:hover .list-group-item-heading > small, .list-group-item.active:focus .list-group-item-heading > small, .list-group-item.active .list-group-item-heading > .small, .list-group-item.active:hover .list-group-item-heading > .small, .list-group-item.active:focus .list-group-item-heading > .small { color: inherit; } .list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text { color: #c7ddef; } .list-group-item-success { color: #3c763d; background-color: #dff0d8; } a.list-group-item-success, button.list-group-item-success { color: #3c763d; } a.list-group-item-success .list-group-item-heading, button.list-group-item-success .list-group-item-heading { color: inherit; } a.list-group-item-success:hover, button.list-group-item-success:hover, a.list-group-item-success:focus, button.list-group-item-success:focus { color: #3c763d; background-color: #d0e9c6; } a.list-group-item-success.active, button.list-group-item-success.active, a.list-group-item-success.active:hover, button.list-group-item-success.active:hover, a.list-group-item-success.active:focus, button.list-group-item-success.active:focus { color: #fff; background-color: #3c763d; border-color: #3c763d; } .list-group-item-info { color: #31708f; background-color: #d9edf7; } a.list-group-item-info, button.list-group-item-info { color: #31708f; } a.list-group-item-info .list-group-item-heading, button.list-group-item-info .list-group-item-heading { color: inherit; } a.list-group-item-info:hover, button.list-group-item-info:hover, a.list-group-item-info:focus, button.list-group-item-info:focus { color: #31708f; background-color: #c4e3f3; } a.list-group-item-info.active, button.list-group-item-info.active, a.list-group-item-info.active:hover, button.list-group-item-info.active:hover, a.list-group-item-info.active:focus, button.list-group-item-info.active:focus { color: #fff; background-color: #31708f; border-color: #31708f; } .list-group-item-warning { color: #8a6d3b; background-color: #fcf8e3; } a.list-group-item-warning, button.list-group-item-warning { color: #8a6d3b; } a.list-group-item-warning .list-group-item-heading, button.list-group-item-warning .list-group-item-heading { color: inherit; } a.list-group-item-warning:hover, button.list-group-item-warning:hover, a.list-group-item-warning:focus, button.list-group-item-warning:focus { color: #8a6d3b; background-color: #faf2cc; } a.list-group-item-warning.active, button.list-group-item-warning.active, a.list-group-item-warning.active:hover, button.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus, button.list-group-item-warning.active:focus { color: #fff; background-color: #8a6d3b; border-color: #8a6d3b; } .list-group-item-danger { color: #a94442; background-color: #f2dede; } a.list-group-item-danger, button.list-group-item-danger { color: #a94442; } a.list-group-item-danger .list-group-item-heading, button.list-group-item-danger .list-group-item-heading { color: inherit; } a.list-group-item-danger:hover, button.list-group-item-danger:hover, a.list-group-item-danger:focus, button.list-group-item-danger:focus { color: #a94442; background-color: #ebcccc; } a.list-group-item-danger.active, button.list-group-item-danger.active, a.list-group-item-danger.active:hover, button.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus, button.list-group-item-danger.active:focus { color: #fff; background-color: #a94442; border-color: #a94442; } .list-group-item-heading { margin-top: 0; margin-bottom: 5px; } .list-group-item-text { margin-bottom: 0; line-height: 1.3; } .panel { margin-bottom: 20px; background-color: #fff; border: 1px solid transparent; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); box-shadow: 0 1px 1px rgba(0, 0, 0, .05); } .panel-body { padding: 15px; } .panel-heading { padding: 10px 15px; border-bottom: 1px solid transparent; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel-heading > .dropdown .dropdown-toggle { color: inherit; } .panel-title { margin-top: 0; margin-bottom: 0; font-size: 16px; color: inherit; } .panel-title > a, .panel-title > small, .panel-title > .small, .panel-title > small > a, .panel-title > .small > a { color: inherit; } .panel-footer { padding: 10px 15px; background-color: #f5f5f5; border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .list-group, .panel > .panel-collapse > .list-group { margin-bottom: 0; } .panel > .list-group .list-group-item, .panel > .panel-collapse > .list-group .list-group-item { border-width: 1px 0; border-radius: 0; } .panel > .list-group:first-child .list-group-item:first-child, .panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { border-top: 0; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .list-group:last-child .list-group-item:last-child, .panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { border-bottom: 0; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .panel-heading + .list-group .list-group-item:first-child { border-top-width: 0; } .list-group + .panel-footer { border-top-width: 0; } .panel > .table, .panel > .table-responsive > .table, .panel > .panel-collapse > .table { margin-bottom: 0; } .panel > .table caption, .panel > .table-responsive > .table caption, .panel > .panel-collapse > .table caption { padding-right: 15px; padding-left: 15px; } .panel > .table:first-child, .panel > .table-responsive:first-child > .table:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { border-top-left-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { border-top-right-radius: 3px; } .panel > .table:last-child, .panel > .table-responsive:last-child > .table:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { border-bottom-right-radius: 3px; } .panel > .panel-body + .table, .panel > .panel-body + .table-responsive, .panel > .table + .panel-body, .panel > .table-responsive + .panel-body { border-top: 1px solid #ddd; } .panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td { border-top: 0; } .panel > .table-bordered, .panel > .table-responsive > .table-bordered { border: 0; } .panel > .table-bordered > thead > tr > th:first-child, .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, .panel > .table-bordered > tbody > tr > th:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, .panel > .table-bordered > tfoot > tr > th:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, .panel > .table-bordered > thead > tr > td:first-child, .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, .panel > .table-bordered > tbody > tr > td:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, .panel > .table-bordered > tfoot > tr > td:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .panel > .table-bordered > thead > tr > th:last-child, .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, .panel > .table-bordered > tbody > tr > th:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, .panel > .table-bordered > tfoot > tr > th:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, .panel > .table-bordered > thead > tr > td:last-child, .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, .panel > .table-bordered > tbody > tr > td:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, .panel > .table-bordered > tfoot > tr > td:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .panel > .table-bordered > thead > tr:first-child > td, .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, .panel > .table-bordered > tbody > tr:first-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, .panel > .table-bordered > thead > tr:first-child > th, .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, .panel > .table-bordered > tbody > tr:first-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { border-bottom: 0; } .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > td, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, .panel > .table-bordered > tbody > tr:last-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, .panel > .table-bordered > tfoot > tr:last-child > th, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { border-bottom: 0; } .panel > .table-responsive { margin-bottom: 0; border: 0; } .panel-group { margin-bottom: 20px; } .panel-group .panel { margin-bottom: 0; border-radius: 4px; } .panel-group .panel + .panel { margin-top: 5px; } .panel-group .panel-heading { border-bottom: 0; } .panel-group .panel-heading + .panel-collapse > .panel-body, .panel-group .panel-heading + .panel-collapse > .list-group { border-top: 1px solid #ddd; } .panel-group .panel-footer { border-top: 0; } .panel-group .panel-footer + .panel-collapse .panel-body { border-bottom: 1px solid #ddd; } .panel-default { border-color: #ddd; } .panel-default > .panel-heading { color: #333; background-color: #f5f5f5; border-color: #ddd; } .panel-default > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ddd; } .panel-default > .panel-heading .badge { color: #f5f5f5; background-color: #333; } .panel-default > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ddd; } .panel-primary { border-color: #337ab7; } .panel-primary > .panel-heading { color: #fff; background-color: #337ab7; border-color: #337ab7; } .panel-primary > .panel-heading + .panel-collapse > .panel-body { border-top-color: #337ab7; } .panel-primary > .panel-heading .badge { color: #337ab7; background-color: #fff; } .panel-primary > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #337ab7; } .panel-success { border-color: #d6e9c6; } .panel-success > .panel-heading { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .panel-success > .panel-heading + .panel-collapse > .panel-body { border-top-color: #d6e9c6; } .panel-success > .panel-heading .badge { color: #dff0d8; background-color: #3c763d; } .panel-success > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #d6e9c6; } .panel-info { border-color: #bce8f1; } .panel-info > .panel-heading { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .panel-info > .panel-heading + .panel-collapse > .panel-body { border-top-color: #bce8f1; } .panel-info > .panel-heading .badge { color: #d9edf7; background-color: #31708f; } .panel-info > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #bce8f1; } .panel-warning { border-color: #faebcc; } .panel-warning > .panel-heading { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .panel-warning > .panel-heading + .panel-collapse > .panel-body { border-top-color: #faebcc; } .panel-warning > .panel-heading .badge { color: #fcf8e3; background-color: #8a6d3b; } .panel-warning > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #faebcc; } .panel-danger { border-color: #ebccd1; } .panel-danger > .panel-heading { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .panel-danger > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ebccd1; } .panel-danger > .panel-heading .badge { color: #f2dede; background-color: #a94442; } .panel-danger > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ebccd1; } .embed-responsive { position: relative; display: block; height: 0; padding: 0; overflow: hidden; } .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } .embed-responsive-16by9 { padding-bottom: 56.25%; } .embed-responsive-4by3 { padding-bottom: 75%; } .well { min-height: 20px; padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #e3e3e3; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); } .well blockquote { border-color: #ddd; border-color: rgba(0, 0, 0, .15); } .well-lg { padding: 24px; border-radius: 6px; } .well-sm { padding: 9px; border-radius: 3px; } .close { float: right; font-size: 21px; font-weight: bold; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; filter: alpha(opacity=20); opacity: .2; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; filter: alpha(opacity=50); opacity: .5; } button.close { -webkit-appearance: none; padding: 0; cursor: pointer; background: transparent; border: 0; } .modal-open { overflow: hidden; } .modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1050; display: none; overflow: hidden; -webkit-overflow-scrolling: touch; outline: 0; } .modal.fade .modal-dialog { -webkit-transition: -webkit-transform .3s ease-out; -o-transition: -o-transform .3s ease-out; transition: transform .3s ease-out; -webkit-transform: translate(0, -25%); -ms-transform: translate(0, -25%); -o-transform: translate(0, -25%); transform: translate(0, -25%); } .modal.in .modal-dialog { -webkit-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); } .modal-open .modal { overflow-x: hidden; overflow-y: auto; } .modal-dialog { position: relative; width: auto; margin: 10px; } .modal-content { position: relative; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; outline: 0; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000; } .modal-backdrop.fade { filter: alpha(opacity=0); opacity: 0; } .modal-backdrop.in { filter: alpha(opacity=50); opacity: .5; } .modal-header { padding: 15px; border-bottom: 1px solid #e5e5e5; } .modal-header .close { margin-top: -2px; } .modal-title { margin: 0; line-height: 1.42857143; } .modal-body { position: relative; padding: 15px; } .modal-footer { padding: 15px; text-align: right; border-top: 1px solid #e5e5e5; } .modal-footer .btn + .btn { margin-bottom: 0; margin-left: 5px; } .modal-footer .btn-group .btn + .btn { margin-left: -1px; } .modal-footer .btn-block + .btn-block { margin-left: 0; } .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } @media (min-width: 768px) { .modal-dialog { width: 600px; margin: 30px auto; } .modal-content { -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); box-shadow: 0 5px 15px rgba(0, 0, 0, .5); } .modal-sm { width: 300px; } } @media (min-width: 992px) { .modal-lg { width: 900px; } } .tooltip { position: absolute; z-index: 1070; display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; filter: alpha(opacity=0); opacity: 0; line-break: auto; } .tooltip.in { filter: alpha(opacity=90); opacity: .9; } .tooltip.top { padding: 5px 0; margin-top: -3px; } .tooltip.right { padding: 0 5px; margin-left: 3px; } .tooltip.bottom { padding: 5px 0; margin-top: 3px; } .tooltip.left { padding: 0 5px; margin-left: -3px; } .tooltip-inner { max-width: 200px; padding: 3px 8px; color: #fff; text-align: center; background-color: #000; border-radius: 4px; } .tooltip-arrow { position: absolute; width: 0; height: 0; border-color: transparent; border-style: solid; } .tooltip.top .tooltip-arrow { bottom: 0; left: 50%; margin-left: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-left .tooltip-arrow { right: 5px; bottom: 0; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-right .tooltip-arrow { bottom: 0; left: 5px; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-width: 5px 5px 5px 0; border-right-color: #000; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-width: 5px 0 5px 5px; border-left-color: #000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-left .tooltip-arrow { top: 0; right: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-right .tooltip-arrow { top: 0; left: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .popover { position: absolute; top: 0; left: 0; z-index: 1060; display: none; max-width: 276px; padding: 1px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); line-break: auto; } .popover.top { margin-top: -10px; } .popover.right { margin-left: 10px; } .popover.bottom { margin-top: 10px; } .popover.left { margin-left: -10px; } .popover-title { padding: 8px 14px; margin: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; } .popover-content { padding: 9px 14px; } .popover > .arrow, .popover > .arrow:after { position: absolute; display: block; width: 0; height: 0; border-color: transparent; border-style: solid; } .popover > .arrow { border-width: 11px; } .popover > .arrow:after { content: ""; border-width: 10px; } .popover.top > .arrow { bottom: -11px; left: 50%; margin-left: -11px; border-top-color: #999; border-top-color: rgba(0, 0, 0, .25); border-bottom-width: 0; } .popover.top > .arrow:after { bottom: 1px; margin-left: -10px; content: " "; border-top-color: #fff; border-bottom-width: 0; } .popover.right > .arrow { top: 50%; left: -11px; margin-top: -11px; border-right-color: #999; border-right-color: rgba(0, 0, 0, .25); border-left-width: 0; } .popover.right > .arrow:after { bottom: -10px; left: 1px; content: " "; border-right-color: #fff; border-left-width: 0; } .popover.bottom > .arrow { top: -11px; left: 50%; margin-left: -11px; border-top-width: 0; border-bottom-color: #999; border-bottom-color: rgba(0, 0, 0, .25); } .popover.bottom > .arrow:after { top: 1px; margin-left: -10px; content: " "; border-top-width: 0; border-bottom-color: #fff; } .popover.left > .arrow { top: 50%; right: -11px; margin-top: -11px; border-right-width: 0; border-left-color: #999; border-left-color: rgba(0, 0, 0, .25); } .popover.left > .arrow:after { right: 1px; bottom: -10px; content: " "; border-right-width: 0; border-left-color: #fff; } .carousel { position: relative; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel-inner > .item { position: relative; display: none; -webkit-transition: .6s ease-in-out left; -o-transition: .6s ease-in-out left; transition: .6s ease-in-out left; } .carousel-inner > .item > img, .carousel-inner > .item > a > img { line-height: 1; } @media all and (transform-3d), (-webkit-transform-3d) { .carousel-inner > .item { -webkit-transition: -webkit-transform .6s ease-in-out; -o-transition: -o-transform .6s ease-in-out; transition: transform .6s ease-in-out; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; perspective: 1000px; } .carousel-inner > .item.next, .carousel-inner > .item.active.right { left: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } .carousel-inner > .item.prev, .carousel-inner > .item.active.left { left: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } .carousel-inner > .item.next.left, .carousel-inner > .item.prev.right, .carousel-inner > .item.active { left: 0; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev { display: block; } .carousel-inner > .active { left: 0; } .carousel-inner > .next, .carousel-inner > .prev { position: absolute; top: 0; width: 100%; } .carousel-inner > .next { left: 100%; } .carousel-inner > .prev { left: -100%; } .carousel-inner > .next.left, .carousel-inner > .prev.right { left: 0; } .carousel-inner > .active.left { left: -100%; } .carousel-inner > .active.right { left: 100%; } .carousel-control { position: absolute; top: 0; bottom: 0; left: 0; width: 15%; font-size: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); background-color: rgba(0, 0, 0, 0); filter: alpha(opacity=50); opacity: .5; } .carousel-control.left { background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); background-repeat: repeat-x; } .carousel-control.right { right: 0; left: auto; background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); background-repeat: repeat-x; } .carousel-control:hover, .carousel-control:focus { color: #fff; text-decoration: none; filter: alpha(opacity=90); outline: 0; opacity: .9; } .carousel-control .icon-prev, .carousel-control .icon-next, .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right { position: absolute; top: 50%; z-index: 5; display: inline-block; margin-top: -10px; } .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { left: 50%; margin-left: -10px; } .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right { right: 50%; margin-right: -10px; } .carousel-control .icon-prev, .carousel-control .icon-next { width: 20px; height: 20px; font-family: serif; line-height: 1; } .carousel-control .icon-prev:before { content: '\2039'; } .carousel-control .icon-next:before { content: '\203a'; } .carousel-indicators { position: absolute; bottom: 10px; left: 50%; z-index: 15; width: 60%; padding-left: 0; margin-left: -30%; text-align: center; list-style: none; } .carousel-indicators li { display: inline-block; width: 10px; height: 10px; margin: 1px; text-indent: -999px; cursor: pointer; background-color: #000 \9; background-color: rgba(0, 0, 0, 0); border: 1px solid #fff; border-radius: 10px; } .carousel-indicators .active { width: 12px; height: 12px; margin: 0; background-color: #fff; } .carousel-caption { position: absolute; right: 15%; bottom: 20px; left: 15%; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); } .carousel-caption .btn { text-shadow: none; } @media screen and (min-width: 768px) { .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next { width: 30px; height: 30px; margin-top: -10px; font-size: 30px; } .carousel-control .glyphicon-chevron-left, .carousel-control .icon-prev { margin-left: -10px; } .carousel-control .glyphicon-chevron-right, .carousel-control .icon-next { margin-right: -10px; } .carousel-caption { right: 20%; left: 20%; padding-bottom: 30px; } .carousel-indicators { bottom: 20px; } } .clearfix:before, .clearfix:after, .dl-horizontal dd:before, .dl-horizontal dd:after, .container:before, .container:after, .container-fluid:before, .container-fluid:after, .row:before, .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, .btn-toolbar:before, .btn-toolbar:after, .btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after, .nav:before, .nav:after, .navbar:before, .navbar:after, .navbar-header:before, .navbar-header:after, .navbar-collapse:before, .navbar-collapse:after, .pager:before, .pager:after, .panel-body:before, .panel-body:after, .modal-header:before, .modal-header:after, .modal-footer:before, .modal-footer:after { display: table; content: " "; } .clearfix:after, .dl-horizontal dd:after, .container:after, .container-fluid:after, .row:after, .form-horizontal .form-group:after, .btn-toolbar:after, .btn-group-vertical > .btn-group:after, .nav:after, .navbar:after, .navbar-header:after, .navbar-collapse:after, .pager:after, .panel-body:after, .modal-header:after, .modal-footer:after { clear: both; } .center-block { display: block; margin-right: auto; margin-left: auto; } .pull-right { float: right !important; } .pull-left { float: left !important; } .hide { display: none !important; } .show { display: block !important; } .invisible { visibility: hidden; } .text-hide { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .hidden { display: none !important; } .affix { position: fixed; } @-ms-viewport { width: device-width; } .visible-xs, .visible-sm, .visible-md, .visible-lg { display: none !important; } .visible-xs-block, .visible-xs-inline, .visible-xs-inline-block, .visible-sm-block, .visible-sm-inline, .visible-sm-inline-block, .visible-md-block, .visible-md-inline, .visible-md-inline-block, .visible-lg-block, .visible-lg-inline, .visible-lg-inline-block { display: none !important; } @media (max-width: 767px) { .visible-xs { display: block !important; } table.visible-xs { display: table !important; } tr.visible-xs { display: table-row !important; } th.visible-xs, td.visible-xs { display: table-cell !important; } } @media (max-width: 767px) { .visible-xs-block { display: block !important; } } @media (max-width: 767px) { .visible-xs-inline { display: inline !important; } } @media (max-width: 767px) { .visible-xs-inline-block { display: inline-block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm { display: block !important; } table.visible-sm { display: table !important; } tr.visible-sm { display: table-row !important; } th.visible-sm, td.visible-sm { display: table-cell !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-block { display: block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline { display: inline !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline-block { display: inline-block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md { display: block !important; } table.visible-md { display: table !important; } tr.visible-md { display: table-row !important; } th.visible-md, td.visible-md { display: table-cell !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-block { display: block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline { display: inline !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline-block { display: inline-block !important; } } @media (min-width: 1200px) { .visible-lg { display: block !important; } table.visible-lg { display: table !important; } tr.visible-lg { display: table-row !important; } th.visible-lg, td.visible-lg { display: table-cell !important; } } @media (min-width: 1200px) { .visible-lg-block { display: block !important; } } @media (min-width: 1200px) { .visible-lg-inline { display: inline !important; } } @media (min-width: 1200px) { .visible-lg-inline-block { display: inline-block !important; } } @media (max-width: 767px) { .hidden-xs { display: none !important; } } @media (min-width: 768px) and (max-width: 991px) { .hidden-sm { display: none !important; } } @media (min-width: 992px) and (max-width: 1199px) { .hidden-md { display: none !important; } } @media (min-width: 1200px) { .hidden-lg { display: none !important; } } .visible-print { display: none !important; } @media print { .visible-print { display: block !important; } table.visible-print { display: table !important; } tr.visible-print { display: table-row !important; } th.visible-print, td.visible-print { display: table-cell !important; } } .visible-print-block { display: none !important; } @media print { .visible-print-block { display: block !important; } } .visible-print-inline { display: none !important; } @media print { .visible-print-inline { display: inline !important; } } .visible-print-inline-block { display: none !important; } @media print { .visible-print-inline-block { display: inline-block !important; } } @media print { .hidden-print { display: none !important; } } /*# sourceMappingURL=bootstrap.css.map */ ================================================ FILE: static/bootstrap/js/bootstrap.js ================================================ /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under the MIT license */ if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } +function ($) { 'use strict'; var version = $.fn.jquery.split(' ')[0].split('.') if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') } }(jQuery); /* ======================================================================== * Bootstrap: transition.js v3.3.7 * http://getbootstrap.com/javascript/#transitions * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) // ============================================================ function transitionEnd() { var el = document.createElement('bootstrap') var transEndEventNames = { WebkitTransition : 'webkitTransitionEnd', MozTransition : 'transitionend', OTransition : 'oTransitionEnd otransitionend', transition : 'transitionend' } for (var name in transEndEventNames) { if (el.style[name] !== undefined) { return { end: transEndEventNames[name] } } } return false // explicit for ie8 ( ._.) } // http://blog.alexmaccaw.com/css-transitions $.fn.emulateTransitionEnd = function (duration) { var called = false var $el = this $(this).one('bsTransitionEnd', function () { called = true }) var callback = function () { if (!called) $($el).trigger($.support.transition.end) } setTimeout(callback, duration) return this } $(function () { $.support.transition = transitionEnd() if (!$.support.transition) return $.event.special.bsTransitionEnd = { bindType: $.support.transition.end, delegateType: $.support.transition.end, handle: function (e) { if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) } } }) }(jQuery); /* ======================================================================== * Bootstrap: alert.js v3.3.7 * http://getbootstrap.com/javascript/#alerts * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // ALERT CLASS DEFINITION // ====================== var dismiss = '[data-dismiss="alert"]' var Alert = function (el) { $(el).on('click', dismiss, this.close) } Alert.VERSION = '3.3.7' Alert.TRANSITION_DURATION = 150 Alert.prototype.close = function (e) { var $this = $(this) var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = $(selector === '#' ? [] : selector) if (e) e.preventDefault() if (!$parent.length) { $parent = $this.closest('.alert') } $parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { // detach from parent, fire event then clean up data $parent.detach().trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? $parent .one('bsTransitionEnd', removeElement) .emulateTransitionEnd(Alert.TRANSITION_DURATION) : removeElement() } // ALERT PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.alert') if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.alert $.fn.alert = Plugin $.fn.alert.Constructor = Alert // ALERT NO CONFLICT // ================= $.fn.alert.noConflict = function () { $.fn.alert = old return this } // ALERT DATA-API // ============== $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) }(jQuery); /* ======================================================================== * Bootstrap: button.js v3.3.7 * http://getbootstrap.com/javascript/#buttons * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // BUTTON PUBLIC CLASS DEFINITION // ============================== var Button = function (element, options) { this.$element = $(element) this.options = $.extend({}, Button.DEFAULTS, options) this.isLoading = false } Button.VERSION = '3.3.7' Button.DEFAULTS = { loadingText: 'loading...' } Button.prototype.setState = function (state) { var d = 'disabled' var $el = this.$element var val = $el.is('input') ? 'val' : 'html' var data = $el.data() state += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) // push to event loop to allow forms to submit setTimeout($.proxy(function () { $el[val](data[state] == null ? this.options[state] : data[state]) if (state == 'loadingText') { this.isLoading = true $el.addClass(d).attr(d, d).prop(d, true) } else if (this.isLoading) { this.isLoading = false $el.removeClass(d).removeAttr(d).prop(d, false) } }, this), 0) } Button.prototype.toggle = function () { var changed = true var $parent = this.$element.closest('[data-toggle="buttons"]') if ($parent.length) { var $input = this.$element.find('input') if ($input.prop('type') == 'radio') { if ($input.prop('checked')) changed = false $parent.find('.active').removeClass('active') this.$element.addClass('active') } else if ($input.prop('type') == 'checkbox') { if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false this.$element.toggleClass('active') } $input.prop('checked', this.$element.hasClass('active')) if (changed) $input.trigger('change') } else { this.$element.attr('aria-pressed', !this.$element.hasClass('active')) this.$element.toggleClass('active') } } // BUTTON PLUGIN DEFINITION // ======================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.button') var options = typeof option == 'object' && option if (!data) $this.data('bs.button', (data = new Button(this, options))) if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } var old = $.fn.button $.fn.button = Plugin $.fn.button.Constructor = Button // BUTTON NO CONFLICT // ================== $.fn.button.noConflict = function () { $.fn.button = old return this } // BUTTON DATA-API // =============== $(document) .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { var $btn = $(e.target).closest('.btn') Plugin.call($btn, 'toggle') if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { // Prevent double click on radios, and the double selections (so cancellation) on checkboxes e.preventDefault() // The target component still receive the focus if ($btn.is('input,button')) $btn.trigger('focus') else $btn.find('input:visible,button:visible').first().trigger('focus') } }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) }) }(jQuery); /* ======================================================================== * Bootstrap: carousel.js v3.3.7 * http://getbootstrap.com/javascript/#carousel * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CAROUSEL CLASS DEFINITION // ========================= var Carousel = function (element, options) { this.$element = $(element) this.$indicators = this.$element.find('.carousel-indicators') this.options = options this.paused = null this.sliding = null this.interval = null this.$active = null this.$items = null this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) } Carousel.VERSION = '3.3.7' Carousel.TRANSITION_DURATION = 600 Carousel.DEFAULTS = { interval: 5000, pause: 'hover', wrap: true, keyboard: true } Carousel.prototype.keydown = function (e) { if (/input|textarea/i.test(e.target.tagName)) return switch (e.which) { case 37: this.prev(); break case 39: this.next(); break default: return } e.preventDefault() } Carousel.prototype.cycle = function (e) { e || (this.paused = false) this.interval && clearInterval(this.interval) this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) return this } Carousel.prototype.getItemIndex = function (item) { this.$items = item.parent().children('.item') return this.$items.index(item || this.$active) } Carousel.prototype.getItemForDirection = function (direction, active) { var activeIndex = this.getItemIndex(active) var willWrap = (direction == 'prev' && activeIndex === 0) || (direction == 'next' && activeIndex == (this.$items.length - 1)) if (willWrap && !this.options.wrap) return active var delta = direction == 'prev' ? -1 : 1 var itemIndex = (activeIndex + delta) % this.$items.length return this.$items.eq(itemIndex) } Carousel.prototype.to = function (pos) { var that = this var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) if (pos > (this.$items.length - 1) || pos < 0) return if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" if (activeIndex == pos) return this.pause().cycle() return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) } Carousel.prototype.pause = function (e) { e || (this.paused = true) if (this.$element.find('.next, .prev').length && $.support.transition) { this.$element.trigger($.support.transition.end) this.cycle(true) } this.interval = clearInterval(this.interval) return this } Carousel.prototype.next = function () { if (this.sliding) return return this.slide('next') } Carousel.prototype.prev = function () { if (this.sliding) return return this.slide('prev') } Carousel.prototype.slide = function (type, next) { var $active = this.$element.find('.item.active') var $next = next || this.getItemForDirection(type, $active) var isCycling = this.interval var direction = type == 'next' ? 'left' : 'right' var that = this if ($next.hasClass('active')) return (this.sliding = false) var relatedTarget = $next[0] var slideEvent = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) this.$element.trigger(slideEvent) if (slideEvent.isDefaultPrevented()) return this.sliding = true isCycling && this.pause() if (this.$indicators.length) { this.$indicators.find('.active').removeClass('active') var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) $nextIndicator && $nextIndicator.addClass('active') } var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" if ($.support.transition && this.$element.hasClass('slide')) { $next.addClass(type) $next[0].offsetWidth // force reflow $active.addClass(direction) $next.addClass(direction) $active .one('bsTransitionEnd', function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger(slidEvent) }, 0) }) .emulateTransitionEnd(Carousel.TRANSITION_DURATION) } else { $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger(slidEvent) } isCycling && this.cycle() return this } // CAROUSEL PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.carousel') var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) var action = typeof option == 'string' ? option : options.slide if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.pause().cycle() }) } var old = $.fn.carousel $.fn.carousel = Plugin $.fn.carousel.Constructor = Carousel // CAROUSEL NO CONFLICT // ==================== $.fn.carousel.noConflict = function () { $.fn.carousel = old return this } // CAROUSEL DATA-API // ================= var clickHandler = function (e) { var href var $this = $(this) var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') if (slideIndex) options.interval = false Plugin.call($target, options) if (slideIndex) { $target.data('bs.carousel').to(slideIndex) } e.preventDefault() } $(document) .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) $(window).on('load', function () { $('[data-ride="carousel"]').each(function () { var $carousel = $(this) Plugin.call($carousel, $carousel.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: collapse.js v3.3.7 * http://getbootstrap.com/javascript/#collapse * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ /* jshint latedef: false */ +function ($) { 'use strict'; // COLLAPSE PUBLIC CLASS DEFINITION // ================================ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, Collapse.DEFAULTS, options) this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + '[data-toggle="collapse"][data-target="#' + element.id + '"]') this.transitioning = null if (this.options.parent) { this.$parent = this.getParent() } else { this.addAriaAndCollapsedClass(this.$element, this.$trigger) } if (this.options.toggle) this.toggle() } Collapse.VERSION = '3.3.7' Collapse.TRANSITION_DURATION = 350 Collapse.DEFAULTS = { toggle: true } Collapse.prototype.dimension = function () { var hasWidth = this.$element.hasClass('width') return hasWidth ? 'width' : 'height' } Collapse.prototype.show = function () { if (this.transitioning || this.$element.hasClass('in')) return var activesData var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') if (actives && actives.length) { activesData = actives.data('bs.collapse') if (activesData && activesData.transitioning) return } var startEvent = $.Event('show.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return if (actives && actives.length) { Plugin.call(actives, 'hide') activesData || actives.data('bs.collapse', null) } var dimension = this.dimension() this.$element .removeClass('collapse') .addClass('collapsing')[dimension](0) .attr('aria-expanded', true) this.$trigger .removeClass('collapsed') .attr('aria-expanded', true) this.transitioning = 1 var complete = function () { this.$element .removeClass('collapsing') .addClass('collapse in')[dimension]('') this.transitioning = 0 this.$element .trigger('shown.bs.collapse') } if (!$.support.transition) return complete.call(this) var scrollSize = $.camelCase(['scroll', dimension].join('-')) this.$element .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) } Collapse.prototype.hide = function () { if (this.transitioning || !this.$element.hasClass('in')) return var startEvent = $.Event('hide.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return var dimension = this.dimension() this.$element[dimension](this.$element[dimension]())[0].offsetHeight this.$element .addClass('collapsing') .removeClass('collapse in') .attr('aria-expanded', false) this.$trigger .addClass('collapsed') .attr('aria-expanded', false) this.transitioning = 1 var complete = function () { this.transitioning = 0 this.$element .removeClass('collapsing') .addClass('collapse') .trigger('hidden.bs.collapse') } if (!$.support.transition) return complete.call(this) this.$element [dimension](0) .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION) } Collapse.prototype.toggle = function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } Collapse.prototype.getParent = function () { return $(this.options.parent) .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') .each($.proxy(function (i, element) { var $element = $(element) this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) }, this)) .end() } Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { var isOpen = $element.hasClass('in') $element.attr('aria-expanded', isOpen) $trigger .toggleClass('collapsed', !isOpen) .attr('aria-expanded', isOpen) } function getTargetFromTrigger($trigger) { var href var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 return $(target) } // COLLAPSE PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.collapse') var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.collapse $.fn.collapse = Plugin $.fn.collapse.Constructor = Collapse // COLLAPSE NO CONFLICT // ==================== $.fn.collapse.noConflict = function () { $.fn.collapse = old return this } // COLLAPSE DATA-API // ================= $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { var $this = $(this) if (!$this.attr('data-target')) e.preventDefault() var $target = getTargetFromTrigger($this) var data = $target.data('bs.collapse') var option = data ? 'toggle' : $this.data() Plugin.call($target, option) }) }(jQuery); /* ======================================================================== * Bootstrap: dropdown.js v3.3.7 * http://getbootstrap.com/javascript/#dropdowns * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // DROPDOWN CLASS DEFINITION // ========================= var backdrop = '.dropdown-backdrop' var toggle = '[data-toggle="dropdown"]' var Dropdown = function (element) { $(element).on('click.bs.dropdown', this.toggle) } Dropdown.VERSION = '3.3.7' function getParent($this) { var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = selector && $(selector) return $parent && $parent.length ? $parent : $this.parent() } function clearMenus(e) { if (e && e.which === 3) return $(backdrop).remove() $(toggle).each(function () { var $this = $(this) var $parent = getParent($this) var relatedTarget = { relatedTarget: this } if (!$parent.hasClass('open')) return if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this.attr('aria-expanded', 'false') $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) }) } Dropdown.prototype.toggle = function (e) { var $this = $(this) if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') clearMenus() if (!isActive) { if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { // if mobile we use a backdrop because click events don't delegate $(document.createElement('div')) .addClass('dropdown-backdrop') .insertAfter($(this)) .on('click', clearMenus) } var relatedTarget = { relatedTarget: this } $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this .trigger('focus') .attr('aria-expanded', 'true') $parent .toggleClass('open') .trigger($.Event('shown.bs.dropdown', relatedTarget)) } return false } Dropdown.prototype.keydown = function (e) { if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return var $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') if (!isActive && e.which != 27 || isActive && e.which == 27) { if (e.which == 27) $parent.find(toggle).trigger('focus') return $this.trigger('click') } var desc = ' li:not(.disabled):visible a' var $items = $parent.find('.dropdown-menu' + desc) if (!$items.length) return var index = $items.index(e.target) if (e.which == 38 && index > 0) index-- // up if (e.which == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items.eq(index).trigger('focus') } // DROPDOWN PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.dropdown') if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.dropdown $.fn.dropdown = Plugin $.fn.dropdown.Constructor = Dropdown // DROPDOWN NO CONFLICT // ==================== $.fn.dropdown.noConflict = function () { $.fn.dropdown = old return this } // APPLY TO STANDARD DROPDOWN ELEMENTS // =================================== $(document) .on('click.bs.dropdown.data-api', clearMenus) .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) }(jQuery); /* ======================================================================== * Bootstrap: modal.js v3.3.7 * http://getbootstrap.com/javascript/#modals * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // MODAL CLASS DEFINITION // ====================== var Modal = function (element, options) { this.options = options this.$body = $(document.body) this.$element = $(element) this.$dialog = this.$element.find('.modal-dialog') this.$backdrop = null this.isShown = null this.originalBodyPad = null this.scrollbarWidth = 0 this.ignoreBackdropClick = false if (this.options.remote) { this.$element .find('.modal-content') .load(this.options.remote, $.proxy(function () { this.$element.trigger('loaded.bs.modal') }, this)) } } Modal.VERSION = '3.3.7' Modal.TRANSITION_DURATION = 300 Modal.BACKDROP_TRANSITION_DURATION = 150 Modal.DEFAULTS = { backdrop: true, keyboard: true, show: true } Modal.prototype.toggle = function (_relatedTarget) { return this.isShown ? this.hide() : this.show(_relatedTarget) } Modal.prototype.show = function (_relatedTarget) { var that = this var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return this.isShown = true this.checkScrollbar() this.setScrollbar() this.$body.addClass('modal-open') this.escape() this.resize() this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) this.$dialog.on('mousedown.dismiss.bs.modal', function () { that.$element.one('mouseup.dismiss.bs.modal', function (e) { if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true }) }) this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(that.$body) // don't move modals dom position } that.$element .show() .scrollTop(0) that.adjustDialog() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') that.enforceFocus() var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) transition ? that.$dialog // wait for modal to slide in .one('bsTransitionEnd', function () { that.$element.trigger('focus').trigger(e) }) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : that.$element.trigger('focus').trigger(e) }) } Modal.prototype.hide = function (e) { if (e) e.preventDefault() e = $.Event('hide.bs.modal') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false this.escape() this.resize() $(document).off('focusin.bs.modal') this.$element .removeClass('in') .off('click.dismiss.bs.modal') .off('mouseup.dismiss.bs.modal') this.$dialog.off('mousedown.dismiss.bs.modal') $.support.transition && this.$element.hasClass('fade') ? this.$element .one('bsTransitionEnd', $.proxy(this.hideModal, this)) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : this.hideModal() } Modal.prototype.enforceFocus = function () { $(document) .off('focusin.bs.modal') // guard against infinite focus loop .on('focusin.bs.modal', $.proxy(function (e) { if (document !== e.target && this.$element[0] !== e.target && !this.$element.has(e.target).length) { this.$element.trigger('focus') } }, this)) } Modal.prototype.escape = function () { if (this.isShown && this.options.keyboard) { this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { e.which == 27 && this.hide() }, this)) } else if (!this.isShown) { this.$element.off('keydown.dismiss.bs.modal') } } Modal.prototype.resize = function () { if (this.isShown) { $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) } else { $(window).off('resize.bs.modal') } } Modal.prototype.hideModal = function () { var that = this this.$element.hide() this.backdrop(function () { that.$body.removeClass('modal-open') that.resetAdjustments() that.resetScrollbar() that.$element.trigger('hidden.bs.modal') }) } Modal.prototype.removeBackdrop = function () { this.$backdrop && this.$backdrop.remove() this.$backdrop = null } Modal.prototype.backdrop = function (callback) { var that = this var animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $(document.createElement('div')) .addClass('modal-backdrop ' + animate) .appendTo(this.$body) this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { if (this.ignoreBackdropClick) { this.ignoreBackdropClick = false return } if (e.target !== e.currentTarget) return this.options.backdrop == 'static' ? this.$element[0].focus() : this.hide() }, this)) if (doAnimate) this.$backdrop[0].offsetWidth // force reflow this.$backdrop.addClass('in') if (!callback) return doAnimate ? this.$backdrop .one('bsTransitionEnd', callback) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callback() } else if (!this.isShown && this.$backdrop) { this.$backdrop.removeClass('in') var callbackRemove = function () { that.removeBackdrop() callback && callback() } $.support.transition && this.$element.hasClass('fade') ? this.$backdrop .one('bsTransitionEnd', callbackRemove) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callbackRemove() } else if (callback) { callback() } } // these following methods are used to handle overflowing modals Modal.prototype.handleUpdate = function () { this.adjustDialog() } Modal.prototype.adjustDialog = function () { var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight this.$element.css({ paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' }) } Modal.prototype.resetAdjustments = function () { this.$element.css({ paddingLeft: '', paddingRight: '' }) } Modal.prototype.checkScrollbar = function () { var fullWindowWidth = window.innerWidth if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 var documentElementRect = document.documentElement.getBoundingClientRect() fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) } this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth this.scrollbarWidth = this.measureScrollbar() } Modal.prototype.setScrollbar = function () { var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) this.originalBodyPad = document.body.style.paddingRight || '' if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) } Modal.prototype.resetScrollbar = function () { this.$body.css('padding-right', this.originalBodyPad) } Modal.prototype.measureScrollbar = function () { // thx walsh var scrollDiv = document.createElement('div') scrollDiv.className = 'modal-scrollbar-measure' this.$body.append(scrollDiv) var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth this.$body[0].removeChild(scrollDiv) return scrollbarWidth } // MODAL PLUGIN DEFINITION // ======================= function Plugin(option, _relatedTarget) { return this.each(function () { var $this = $(this) var data = $this.data('bs.modal') var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('bs.modal', (data = new Modal(this, options))) if (typeof option == 'string') data[option](_relatedTarget) else if (options.show) data.show(_relatedTarget) }) } var old = $.fn.modal $.fn.modal = Plugin $.fn.modal.Constructor = Modal // MODAL NO CONFLICT // ================= $.fn.modal.noConflict = function () { $.fn.modal = old return this } // MODAL DATA-API // ============== $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() $target.one('show.bs.modal', function (showEvent) { if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown $target.one('hidden.bs.modal', function () { $this.is(':visible') && $this.trigger('focus') }) }) Plugin.call($target, option, this) }) }(jQuery); /* ======================================================================== * Bootstrap: tooltip.js v3.3.7 * http://getbootstrap.com/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TOOLTIP PUBLIC CLASS DEFINITION // =============================== var Tooltip = function (element, options) { this.type = null this.options = null this.enabled = null this.timeout = null this.hoverState = null this.$element = null this.inState = null this.init('tooltip', element, options) } Tooltip.VERSION = '3.3.7' Tooltip.TRANSITION_DURATION = 150 Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 } } Tooltip.prototype.init = function (type, element, options) { this.enabled = true this.type = type this.$element = $(element) this.options = this.getOptions(options) this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) this.inState = { click: false, hover: false, focus: false } if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') } var triggers = this.options.trigger.split(' ') for (var i = triggers.length; i--;) { var trigger = triggers[i] if (trigger == 'click') { this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) } else if (trigger != 'manual') { var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) } } this.options.selector ? (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : this.fixTitle() } Tooltip.prototype.getDefaults = function () { return Tooltip.DEFAULTS } Tooltip.prototype.getOptions = function (options) { options = $.extend({}, this.getDefaults(), this.$element.data(), options) if (options.delay && typeof options.delay == 'number') { options.delay = { show: options.delay, hide: options.delay } } return options } Tooltip.prototype.getDelegateOptions = function () { var options = {} var defaults = this.getDefaults() this._options && $.each(this._options, function (key, value) { if (defaults[key] != value) options[key] = value }) return options } Tooltip.prototype.enter = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true } if (self.tip().hasClass('in') || self.hoverState == 'in') { self.hoverState = 'in' return } clearTimeout(self.timeout) self.hoverState = 'in' if (!self.options.delay || !self.options.delay.show) return self.show() self.timeout = setTimeout(function () { if (self.hoverState == 'in') self.show() }, self.options.delay.show) } Tooltip.prototype.isInStateTrue = function () { for (var key in this.inState) { if (this.inState[key]) return true } return false } Tooltip.prototype.leave = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false } if (self.isInStateTrue()) return clearTimeout(self.timeout) self.hoverState = 'out' if (!self.options.delay || !self.options.delay.hide) return self.hide() self.timeout = setTimeout(function () { if (self.hoverState == 'out') self.hide() }, self.options.delay.hide) } Tooltip.prototype.show = function () { var e = $.Event('show.bs.' + this.type) if (this.hasContent() && this.enabled) { this.$element.trigger(e) var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) if (e.isDefaultPrevented() || !inDom) return var that = this var $tip = this.tip() var tipId = this.getUID(this.type) this.setContent() $tip.attr('id', tipId) this.$element.attr('aria-describedby', tipId) if (this.options.animation) $tip.addClass('fade') var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement var autoToken = /\s?auto?\s?/i var autoPlace = autoToken.test(placement) if (autoPlace) placement = placement.replace(autoToken, '') || 'top' $tip .detach() .css({ top: 0, left: 0, display: 'block' }) .addClass(placement) .data('bs.' + this.type, this) this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) this.$element.trigger('inserted.bs.' + this.type) var pos = this.getPosition() var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (autoPlace) { var orgPlacement = placement var viewportDim = this.getPosition(this.$viewport) placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement $tip .removeClass(orgPlacement) .addClass(placement) } var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) this.applyPlacement(calculatedOffset, placement) var complete = function () { var prevHoverState = that.hoverState that.$element.trigger('shown.bs.' + that.type) that.hoverState = null if (prevHoverState == 'out') that.leave(that) } $.support.transition && this.$tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() } } Tooltip.prototype.applyPlacement = function (offset, placement) { var $tip = this.tip() var width = $tip[0].offsetWidth var height = $tip[0].offsetHeight // manually read margins because getBoundingClientRect includes difference var marginTop = parseInt($tip.css('margin-top'), 10) var marginLeft = parseInt($tip.css('margin-left'), 10) // we must check for NaN for ie 8/9 if (isNaN(marginTop)) marginTop = 0 if (isNaN(marginLeft)) marginLeft = 0 offset.top += marginTop offset.left += marginLeft // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 $.offset.setOffset($tip[0], $.extend({ using: function (props) { $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) } }, offset), 0) $tip.addClass('in') // check to see if placing tip in new offset caused the tip to resize itself var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (placement == 'top' && actualHeight != height) { offset.top = offset.top + height - actualHeight } var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) if (delta.left) offset.left += delta.left else offset.top += delta.top var isVertical = /top|bottom/.test(placement) var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' $tip.offset(offset) this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) } Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { this.arrow() .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') .css(isVertical ? 'top' : 'left', '') } Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) $tip.removeClass('fade in top bottom left right') } Tooltip.prototype.hide = function (callback) { var that = this var $tip = $(this.$tip) var e = $.Event('hide.bs.' + this.type) function complete() { if (that.hoverState != 'in') $tip.detach() if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary. that.$element .removeAttr('aria-describedby') .trigger('hidden.bs.' + that.type) } callback && callback() } this.$element.trigger(e) if (e.isDefaultPrevented()) return $tip.removeClass('in') $.support.transition && $tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() this.hoverState = null return this } Tooltip.prototype.fixTitle = function () { var $e = this.$element if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') } } Tooltip.prototype.hasContent = function () { return this.getTitle() } Tooltip.prototype.getPosition = function ($element) { $element = $element || this.$element var el = $element[0] var isBody = el.tagName == 'BODY' var elRect = el.getBoundingClientRect() if (elRect.width == null) { // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) } var isSvg = window.SVGElement && el instanceof window.SVGElement // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3. // See https://github.com/twbs/bootstrap/issues/20280 var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset()) var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null return $.extend({}, elRect, scroll, outerDims, elOffset) } Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } } Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { var delta = { top: 0, left: 0 } if (!this.$viewport) return delta var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 var viewportDimensions = this.getPosition(this.$viewport) if (/right|left/.test(placement)) { var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset } } else { var leftEdgeOffset = pos.left - viewportPadding var rightEdgeOffset = pos.left + viewportPadding + actualWidth if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset } } return delta } Tooltip.prototype.getTitle = function () { var title var $e = this.$element var o = this.options title = $e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) return title } Tooltip.prototype.getUID = function (prefix) { do prefix += ~~(Math.random() * 1000000) while (document.getElementById(prefix)) return prefix } Tooltip.prototype.tip = function () { if (!this.$tip) { this.$tip = $(this.options.template) if (this.$tip.length != 1) { throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') } } return this.$tip } Tooltip.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) } Tooltip.prototype.enable = function () { this.enabled = true } Tooltip.prototype.disable = function () { this.enabled = false } Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled } Tooltip.prototype.toggle = function (e) { var self = this if (e) { self = $(e.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(e.currentTarget, this.getDelegateOptions()) $(e.currentTarget).data('bs.' + this.type, self) } } if (e) { self.inState.click = !self.inState.click if (self.isInStateTrue()) self.enter(self) else self.leave(self) } else { self.tip().hasClass('in') ? self.leave(self) : self.enter(self) } } Tooltip.prototype.destroy = function () { var that = this clearTimeout(this.timeout) this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type) if (that.$tip) { that.$tip.detach() } that.$tip = null that.$arrow = null that.$viewport = null that.$element = null }) } // TOOLTIP PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tooltip') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tooltip $.fn.tooltip = Plugin $.fn.tooltip.Constructor = Tooltip // TOOLTIP NO CONFLICT // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old return this } }(jQuery); /* ======================================================================== * Bootstrap: popover.js v3.3.7 * http://getbootstrap.com/javascript/#popovers * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // POPOVER PUBLIC CLASS DEFINITION // =============================== var Popover = function (element, options) { this.init('popover', element, options) } if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') Popover.VERSION = '3.3.7' Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { placement: 'right', trigger: 'click', content: '', template: '' }) // NOTE: POPOVER EXTENDS tooltip.js // ================================ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) Popover.prototype.constructor = Popover Popover.prototype.getDefaults = function () { return Popover.DEFAULTS } Popover.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() var content = this.getContent() $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' ](content) $tip.removeClass('fade top bottom left right in') // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do // this manually by checking the contents. if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() } Popover.prototype.hasContent = function () { return this.getTitle() || this.getContent() } Popover.prototype.getContent = function () { var $e = this.$element var o = this.options return $e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) } Popover.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.arrow')) } // POPOVER PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.popover') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.popover', (data = new Popover(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.popover $.fn.popover = Plugin $.fn.popover.Constructor = Popover // POPOVER NO CONFLICT // =================== $.fn.popover.noConflict = function () { $.fn.popover = old return this } }(jQuery); /* ======================================================================== * Bootstrap: scrollspy.js v3.3.7 * http://getbootstrap.com/javascript/#scrollspy * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // SCROLLSPY CLASS DEFINITION // ========================== function ScrollSpy(element, options) { this.$body = $(document.body) this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) this.options = $.extend({}, ScrollSpy.DEFAULTS, options) this.selector = (this.options.target || '') + ' .nav li > a' this.offsets = [] this.targets = [] this.activeTarget = null this.scrollHeight = 0 this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) this.refresh() this.process() } ScrollSpy.VERSION = '3.3.7' ScrollSpy.DEFAULTS = { offset: 10 } ScrollSpy.prototype.getScrollHeight = function () { return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) } ScrollSpy.prototype.refresh = function () { var that = this var offsetMethod = 'offset' var offsetBase = 0 this.offsets = [] this.targets = [] this.scrollHeight = this.getScrollHeight() if (!$.isWindow(this.$scrollElement[0])) { offsetMethod = 'position' offsetBase = this.$scrollElement.scrollTop() } this.$body .find(this.selector) .map(function () { var $el = $(this) var href = $el.data('target') || $el.attr('href') var $href = /^#./.test(href) && $(href) return ($href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]) || null }) .sort(function (a, b) { return a[0] - b[0] }) .each(function () { that.offsets.push(this[0]) that.targets.push(this[1]) }) } ScrollSpy.prototype.process = function () { var scrollTop = this.$scrollElement.scrollTop() + this.options.offset var scrollHeight = this.getScrollHeight() var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() var offsets = this.offsets var targets = this.targets var activeTarget = this.activeTarget var i if (this.scrollHeight != scrollHeight) { this.refresh() } if (scrollTop >= maxScroll) { return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) } if (activeTarget && scrollTop < offsets[0]) { this.activeTarget = null return this.clear() } for (i = offsets.length; i--;) { activeTarget != targets[i] && scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i]) } } ScrollSpy.prototype.activate = function (target) { this.activeTarget = target this.clear() var selector = this.selector + '[data-target="' + target + '"],' + this.selector + '[href="' + target + '"]' var active = $(selector) .parents('li') .addClass('active') if (active.parent('.dropdown-menu').length) { active = active .closest('li.dropdown') .addClass('active') } active.trigger('activate.bs.scrollspy') } ScrollSpy.prototype.clear = function () { $(this.selector) .parentsUntil(this.options.target, '.active') .removeClass('active') } // SCROLLSPY PLUGIN DEFINITION // =========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.scrollspy') var options = typeof option == 'object' && option if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.scrollspy $.fn.scrollspy = Plugin $.fn.scrollspy.Constructor = ScrollSpy // SCROLLSPY NO CONFLICT // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old return this } // SCROLLSPY DATA-API // ================== $(window).on('load.bs.scrollspy.data-api', function () { $('[data-spy="scroll"]').each(function () { var $spy = $(this) Plugin.call($spy, $spy.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: tab.js v3.3.7 * http://getbootstrap.com/javascript/#tabs * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TAB CLASS DEFINITION // ==================== var Tab = function (element) { // jscs:disable requireDollarBeforejQueryAssignment this.element = $(element) // jscs:enable requireDollarBeforejQueryAssignment } Tab.VERSION = '3.3.7' Tab.TRANSITION_DURATION = 150 Tab.prototype.show = function () { var $this = this.element var $ul = $this.closest('ul:not(.dropdown-menu)') var selector = $this.data('target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } if ($this.parent('li').hasClass('active')) return var $previous = $ul.find('.active:last a') var hideEvent = $.Event('hide.bs.tab', { relatedTarget: $this[0] }) var showEvent = $.Event('show.bs.tab', { relatedTarget: $previous[0] }) $previous.trigger(hideEvent) $this.trigger(showEvent) if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return var $target = $(selector) this.activate($this.closest('li'), $ul) this.activate($target, $target.parent(), function () { $previous.trigger({ type: 'hidden.bs.tab', relatedTarget: $this[0] }) $this.trigger({ type: 'shown.bs.tab', relatedTarget: $previous[0] }) }) } Tab.prototype.activate = function (element, container, callback) { var $active = container.find('> .active') var transition = callback && $.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) function next() { $active .removeClass('active') .find('> .dropdown-menu > .active') .removeClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', false) element .addClass('active') .find('[data-toggle="tab"]') .attr('aria-expanded', true) if (transition) { element[0].offsetWidth // reflow for transition element.addClass('in') } else { element.removeClass('fade') } if (element.parent('.dropdown-menu').length) { element .closest('li.dropdown') .addClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', true) } callback && callback() } $active.length && transition ? $active .one('bsTransitionEnd', next) .emulateTransitionEnd(Tab.TRANSITION_DURATION) : next() $active.removeClass('in') } // TAB PLUGIN DEFINITION // ===================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tab') if (!data) $this.data('bs.tab', (data = new Tab(this))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tab $.fn.tab = Plugin $.fn.tab.Constructor = Tab // TAB NO CONFLICT // =============== $.fn.tab.noConflict = function () { $.fn.tab = old return this } // TAB DATA-API // ============ var clickHandler = function (e) { e.preventDefault() Plugin.call($(this), 'show') } $(document) .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) }(jQuery); /* ======================================================================== * Bootstrap: affix.js v3.3.7 * http://getbootstrap.com/javascript/#affix * ======================================================================== * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // AFFIX CLASS DEFINITION // ====================== var Affix = function (element, options) { this.options = $.extend({}, Affix.DEFAULTS, options) this.$target = $(this.options.target) .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) this.$element = $(element) this.affixed = null this.unpin = null this.pinnedOffset = null this.checkPosition() } Affix.VERSION = '3.3.7' Affix.RESET = 'affix affix-top affix-bottom' Affix.DEFAULTS = { offset: 0, target: window } Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { var scrollTop = this.$target.scrollTop() var position = this.$element.offset() var targetHeight = this.$target.height() if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false if (this.affixed == 'bottom') { if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' } var initializing = this.affixed == null var colliderTop = initializing ? scrollTop : position.top var colliderHeight = initializing ? targetHeight : height if (offsetTop != null && scrollTop <= offsetTop) return 'top' if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' return false } Affix.prototype.getPinnedOffset = function () { if (this.pinnedOffset) return this.pinnedOffset this.$element.removeClass(Affix.RESET).addClass('affix') var scrollTop = this.$target.scrollTop() var position = this.$element.offset() return (this.pinnedOffset = position.top - scrollTop) } Affix.prototype.checkPositionWithEventLoop = function () { setTimeout($.proxy(this.checkPosition, this), 1) } Affix.prototype.checkPosition = function () { if (!this.$element.is(':visible')) return var height = this.$element.height() var offset = this.options.offset var offsetTop = offset.top var offsetBottom = offset.bottom var scrollHeight = Math.max($(document).height(), $(document.body).height()) if (typeof offset != 'object') offsetBottom = offsetTop = offset if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) if (this.affixed != affix) { if (this.unpin != null) this.$element.css('top', '') var affixType = 'affix' + (affix ? '-' + affix : '') var e = $.Event(affixType + '.bs.affix') this.$element.trigger(e) if (e.isDefaultPrevented()) return this.affixed = affix this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null this.$element .removeClass(Affix.RESET) .addClass(affixType) .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') } if (affix == 'bottom') { this.$element.offset({ top: scrollHeight - height - offsetBottom }) } } // AFFIX PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.affix') var options = typeof option == 'object' && option if (!data) $this.data('bs.affix', (data = new Affix(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.affix $.fn.affix = Plugin $.fn.affix.Constructor = Affix // AFFIX NO CONFLICT // ================= $.fn.affix.noConflict = function () { $.fn.affix = old return this } // AFFIX DATA-API // ============== $(window).on('load', function () { $('[data-spy="affix"]').each(function () { var $spy = $(this) var data = $spy.data() data.offset = data.offset || {} if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom if (data.offsetTop != null) data.offset.top = data.offsetTop Plugin.call($spy, data) }) }) }(jQuery); ================================================ FILE: static/bootstrap/js/npm.js ================================================ // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. require('../../js/transition.js') require('../../js/alert.js') require('../../js/button.js') require('../../js/carousel.js') require('../../js/collapse.js') require('../../js/dropdown.js') require('../../js/modal.js') require('../../js/tooltip.js') require('../../js/popover.js') require('../../js/scrollspy.js') require('../../js/tab.js') require('../../js/affix.js') ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/css/fileinput-rtl.css ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee RTL (Right To Left) default styling for bootstrap-fileinput. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ .kv-rtl .close, .kv-rtl .krajee-default .file-actions, .kv-rtl .krajee-default .file-other-error { float: left; } .kv-rtl .krajee-default.file-preview-frame, .kv-rtl .krajee-default .file-drag-handle, .kv-rtl .krajee-default .file-upload-indicator { float: right; } .kv-rtl .file-zoom-dialog, .kv-rtl .file-error-message pre, .kv-rtl .file-error-message ul { text-align: right; } .kv-rtl { direction: rtl; } .kv-rtl .floating-buttons { left: 10px; right: auto; } .kv-rtl .floating-buttons .btn-kv { margin-left: 0; margin-right: 3px; } .kv-rtl .file-caption-icon { left: auto; right: 8px; } .kv-rtl .file-drop-zone { margin: 12px 12px 12px 15px; } .kv-rtl .btn-prev { right: 1px; left: auto; } .kv-rtl .btn-next { left: 1px; right: auto; } .kv-rtl .pull-right, .kv-rtl .float-right { float: left !important; } .kv-rtl .pull-left, .kv-rtl .float-left { float: right !important; } .kv-rtl .kv-zoom-title { direction: ltr; } .kv-rtl .krajee-default.file-preview-frame { box-shadow: -1px 1px 5px 0 #a2958a; } .kv-rtl .krajee-default.file-preview-frame:not(.file-preview-error):hover { box-shadow: -3px 3px 5px 0 #333; } .kv-rtl .kv-zoom-actions .btn-kv { margin-left: 0; margin-right: 3px; } .kv-rtl .file-caption.icon-visible .file-caption-name { padding-left: 0; padding-right: 15px; } .kv-rtl .input-group-btn:last-child > .btn { border-radius: 4px 0 0 4px; } .kv-rtl .input-group .form-control:first-child { border-radius: 0 4px 4px 0; } .kv-rtl .btn-file input[type=file] { right: auto; left: 0; text-align: left; background: none repeat scroll 100% 0 transparent; } ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/css/fileinput.css ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee default styling for bootstrap-fileinput. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ .file-loading input[type=file], input[type=file].file-loading { width: 0; height: 0; } .kv-hidden, .file-caption-icon, .file-zoom-dialog .modal-header:before, .file-zoom-dialog .modal-header:after, .file-input-new .file-preview, .file-input-new .close, .file-input-new .glyphicon-file, .file-input-new .fileinput-remove-button, .file-input-new .fileinput-upload-button, .file-input-new .no-browse .input-group-btn, .file-input-ajax-new .fileinput-remove-button, .file-input-ajax-new .fileinput-upload-button, .file-input-ajax-new .no-browse .input-group-btn, .hide-content .kv-file-content { display: none; } .btn-file input[type=file], .file-caption-icon, .file-preview .fileinput-remove, .krajee-default .file-thumb-progress, .file-zoom-dialog .btn-navigate, .file-zoom-dialog .floating-buttons { position: absolute; } .file-loading:before, .btn-file, .file-caption, .file-preview, .krajee-default.file-preview-frame, .krajee-default .file-thumbnail-footer, .file-zoom-dialog .modal-dialog { position: relative; } .file-error-message pre, .file-error-message ul, .krajee-default .file-actions, .krajee-default .file-other-error { text-align: left; } .file-error-message pre, .file-error-message ul { margin: 0; } .krajee-default .file-drag-handle, .krajee-default .file-upload-indicator { float: left; margin: 5px 0 -5px; width: 16px; height: 16px; } .krajee-default .file-thumb-progress .progress, .krajee-default .file-thumb-progress .progress-bar { height: 11px; font-size: 9px; line-height: 10px; } .krajee-default .file-caption-info, .krajee-default .file-size-info { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 160px; height: 15px; margin: auto; } .file-zoom-content > .file-object.type-video, .file-zoom-content > .file-object.type-flash, .file-zoom-content > .file-object.type-image { max-width: 100%; max-height: 100%; width: auto; } .file-zoom-content > .file-object.type-video, .file-zoom-content > .file-object.type-flash { height: 100%; } .file-zoom-content > .file-object.type-pdf, .file-zoom-content > .file-object.type-html, .file-zoom-content > .file-object.type-text, .file-zoom-content > .file-object.type-default { width: 100%; } .rotate-2 { transform: rotateY(180deg); } .rotate-3 { transform: rotate(180deg); } .rotate-4 { transform: rotate(180deg) rotateY(180deg); } .rotate-5 { transform: rotate(270deg) rotateY(180deg); } .rotate-6 { transform: rotate(90deg); } .rotate-7 { transform: rotate(90deg) rotateY(180deg); } .rotate-8 { transform: rotate(270deg); } .file-loading:before { content: " Loading..."; display: inline-block; padding-left: 20px; line-height: 16px; font-size: 13px; font-variant: small-caps; color: #999; background: transparent url(../img/loading.gif) top left no-repeat; } .file-object { margin: 0 0 -5px 0; padding: 0; } .btn-file { overflow: hidden; } .btn-file input[type=file] { top: 0; right: 0; min-width: 100%; min-height: 100%; text-align: right; opacity: 0; background: none repeat scroll 0 0 transparent; cursor: inherit; display: block; } .btn-file ::-ms-browse { font-size: 10000px; width: 100%; height: 100%; } .file-caption .file-caption-name { width: 100%; margin: 0; padding: 0; box-shadow: none; border: none; background: none; outline: none; } .file-caption.icon-visible .file-caption-icon { display: inline-block; } .file-caption.icon-visible .file-caption-name { padding-left: 15px; } .file-caption-icon { line-height: 1; left: 8px; } .file-error-message { color: #a94442; background-color: #f2dede; margin: 5px; border: 1px solid #ebccd1; border-radius: 4px; padding: 15px; } .file-error-message pre { margin: 5px 0; } .file-caption-disabled { background-color: #eee; cursor: not-allowed; opacity: 1; } .file-preview { border-radius: 5px; border: 1px solid #ddd; padding: 8px; width: 100%; margin-bottom: 5px; } .file-preview .btn-xs { padding: 1px 5px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .file-preview .fileinput-remove { top: 1px; right: 1px; line-height: 10px; } .file-preview .clickable { cursor: pointer; } .file-preview-image { font: 40px Impact, Charcoal, sans-serif; color: #008000; } .krajee-default.file-preview-frame { margin: 8px; border: 1px solid #ddd; box-shadow: 1px 1px 5px 0 #a2958a; padding: 6px; float: left; text-align: center; } .krajee-default.file-preview-frame .kv-file-content { width: 213px; height: 160px; } .krajee-default.file-preview-frame .file-thumbnail-footer { height: 70px; } .krajee-default.file-preview-frame:not(.file-preview-error):hover { box-shadow: 3px 3px 5px 0 #333; } .krajee-default .file-preview-text { display: block; color: #428bca; border: 1px solid #ddd; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; outline: none; padding: 8px; resize: none; } .krajee-default .file-preview-html { border: 1px solid #ddd; padding: 8px; overflow: auto; } .krajee-default .file-other-icon { font-size: 6em; } .krajee-default .file-footer-buttons { float: right; } .krajee-default .file-footer-caption { display: block; text-align: center; padding-top: 4px; font-size: 11px; color: #777; margin-bottom: 15px; } .krajee-default .file-preview-error { opacity: 0.65; box-shadow: none; } .krajee-default .file-thumb-progress { height: 11px; top: 37px; left: 0; right: 0; } .krajee-default.kvsortable-ghost { background: #e1edf7; border: 2px solid #a1abff; } .krajee-default .file-preview-other:hover { opacity: 0.8; } .krajee-default .file-preview-frame:not(.file-preview-error) .file-footer-caption:hover { color: #000; } .kv-upload-progress .progress { height: 20px; line-height: 20px; margin: 10px 0; overflow: hidden; } .kv-upload-progress .progress-bar { height: 20px; line-height: 20px; } /*noinspection CssOverwrittenProperties*/ .file-zoom-dialog .file-other-icon { font-size: 22em; font-size: 50vmin; } .file-zoom-dialog .modal-dialog { width: auto; } .file-zoom-dialog .modal-header { display: flex; align-items: center; justify-content: space-between; } .file-zoom-dialog .btn-navigate { padding: 0; margin: 0; background: transparent; text-decoration: none; outline: none; opacity: 0.7; top: 45%; font-size: 4em; color: #1c94c4; } .file-zoom-dialog .btn-navigate:not([disabled]):hover { outline: none; box-shadow: none; opacity: 0.6; } .file-zoom-dialog .floating-buttons { top: 5px; right: 10px; } .file-zoom-dialog .btn-navigate[disabled] { opacity: 0.3; } .file-zoom-dialog .btn-prev { left: 1px; } .file-zoom-dialog .btn-next { right: 1px; } .file-zoom-dialog .kv-zoom-title { font-weight: 300; color: #999; max-width: 50%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .file-input-new .no-browse .form-control { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .file-input-ajax-new .no-browse .form-control { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .file-caption-main { width: 100%; } .file-thumb-loading { background: transparent url(../img/loading.gif) no-repeat scroll center center content-box !important; } .file-drop-zone { border: 1px dashed #aaa; border-radius: 4px; height: 100%; text-align: center; vertical-align: middle; margin: 12px 15px 12px 12px; padding: 5px; } .file-drop-zone.clickable:hover { border: 2px dashed #999; } .file-drop-zone.clickable:focus { border: 2px solid #5acde2; } .file-drop-zone .file-preview-thumbnails { cursor: default; } .file-drop-zone-title { color: #aaa; font-size: 1.6em; padding: 85px 10px; cursor: default; } .file-highlighted { border: 2px dashed #999 !important; background-color: #eee; } .file-uploading { background: url(../img/loading-sm.gif) no-repeat center bottom 10px; opacity: 0.65; } @media (min-width: 576px) { .file-zoom-dialog .modal-dialog { max-width: 500px; } } @media (min-width: 992px) { .file-zoom-dialog .modal-lg { max-width: 800px; } } .file-zoom-fullscreen.modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .file-zoom-fullscreen .modal-dialog { position: fixed; margin: 0; padding: 0; width: 100%; height: 100%; max-width: 100%; max-height: 100%; } .file-zoom-fullscreen .modal-content { border-radius: 0; box-shadow: none; } .file-zoom-fullscreen .modal-body { overflow-y: auto; } .btn-kv { display: inline-block; text-align: center; width: 30px; height: 30px; line-height: 30px; padding: 0; font-size: 90%; border-radius: 0.2rem; } .floating-buttons { z-index: 3000; } .floating-buttons .btn-kv { margin-left: 3px; z-index: 3000; } .file-zoom-content { height: 480px; text-align: center; } .file-zoom-content .file-preview-image { max-height: 100%; } .file-zoom-content .file-preview-video { max-height: 100%; } .file-zoom-content .is-portrait-gt4 { margin-top: 60px; } .file-zoom-content > .file-object.type-image { height: auto; min-height: inherit; } .file-zoom-content > .file-object.type-audio { width: auto; height: 30px; } @media screen and (max-width: 767px) { .file-preview-thumbnails { display: flex; justify-content: center; align-items: center; flex-direction: column; } .file-zoom-dialog .modal-header { flex-direction: column; } } @media screen and (max-width: 350px) { .krajee-default.file-preview-frame .kv-file-content { width: 160px; } } .file-loading[dir=rtl]:before { background: transparent url(../img/loading.gif) top right no-repeat; padding-left: 0; padding-right: 20px; } .file-sortable .file-drag-handle { cursor: move; opacity: 1; } .file-sortable .file-drag-handle:hover { opacity: 0.7; } .clickable .file-drop-zone-title { cursor: pointer; } .kv-zoom-actions .btn-kv { margin-left: 3px; } .file-preview-initial.sortable-chosen { background-color: #d9edf7; } ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/fileinput.js ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ (function (factory) { "use strict"; //noinspection JSUnresolvedVariable if (typeof define === 'function' && define.amd) { // jshint ignore:line // AMD. Register as an anonymous module. define(['jquery'], factory); // jshint ignore:line } else { // noinspection JSUnresolvedVariable if (typeof module === 'object' && module.exports) { // jshint ignore:line // Node/CommonJS // noinspection JSUnresolvedVariable module.exports = factory(require('jquery')); // jshint ignore:line } else { // Browser globals factory(window.jQuery); } } }(function ($) { "use strict"; $.fn.fileinputLocales = {}; $.fn.fileinputThemes = {}; String.prototype.setTokens = function (replacePairs) { var str = this.toString(), key, re; for (key in replacePairs) { if (replacePairs.hasOwnProperty(key)) { re = new RegExp("\{" + key + "\}", "g"); str = str.replace(re, replacePairs[key]); } } return str; }; var $h, FileInput; // fileinput helper object for all global variables and internal helper methods //noinspection JSUnresolvedVariable $h = { FRAMES: '.kv-preview-thumb', SORT_CSS: 'file-sortable', OBJECT_PARAMS: '\n' + '\n' + '\n' + '\n' + '\n' + '\n', DEFAULT_PREVIEW: '
\n' + '{previewFileIcon}\n' + '
', MODAL_ID: 'kvFileinputModal', MODAL_EVENTS: ['show', 'shown', 'hide', 'hidden', 'loaded'], objUrl: window.URL || window.webkitURL, compare: function (input, str, exact) { return input !== undefined && (exact ? input === str : input.match(str)); }, isIE: function (ver) { // check for IE versions < 11 if (navigator.appName !== 'Microsoft Internet Explorer') { return false; } if (ver === 10) { return new RegExp('msie\\s' + ver, 'i').test(navigator.userAgent); } var div = document.createElement("div"), status; div.innerHTML = ""; status = div.getElementsByTagName("i").length; document.body.appendChild(div); div.parentNode.removeChild(div); return status; }, initModal: function ($modal) { var $body = $('body'); if ($body.length) { $modal.appendTo($body); } }, isEmpty: function (value, trim) { return value === undefined || value === null || value.length === 0 || (trim && $.trim(value) === ''); }, isArray: function (a) { return Array.isArray(a) || Object.prototype.toString.call(a) === '[object Array]'; }, ifSet: function (needle, haystack, def) { def = def || ''; return (haystack && typeof haystack === 'object' && needle in haystack) ? haystack[needle] : def; }, cleanArray: function (arr) { if (!(arr instanceof Array)) { arr = []; } return arr.filter(function (e) { return (e !== undefined && e !== null); }); }, spliceArray: function (arr, index) { var i, j = 0, out = []; if (!(arr instanceof Array)) { return []; } for (i = 0; i < arr.length; i++) { if (i !== index) { out[j] = arr[i]; j++; } } return out; }, getNum: function (num, def) { def = def || 0; if (typeof num === "number") { return num; } if (typeof num === "string") { num = parseFloat(num); } return isNaN(num) ? def : num; }, hasFileAPISupport: function () { return !!(window.File && window.FileReader); }, hasDragDropSupport: function () { var div = document.createElement('div'); /** @namespace div.draggable */ /** @namespace div.ondragstart */ /** @namespace div.ondrop */ return !$h.isIE(9) && (div.draggable !== undefined || (div.ondragstart !== undefined && div.ondrop !== undefined)); }, hasFileUploadSupport: function () { return $h.hasFileAPISupport() && window.FormData; }, hasBlobSupport: function () { try { return !!window.Blob && Boolean(new Blob()); } catch (e) { return false; } }, hasArrayBufferViewSupport: function () { try { return new Blob([new Uint8Array(100)]).size === 100; } catch (e) { return false; } }, dataURI2Blob: function (dataURI) { //noinspection JSUnresolvedVariable var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, canBlob = $h.hasBlobSupport(), byteStr, arrayBuffer, intArray, i, mimeStr, bb, canProceed = (canBlob || BlobBuilder) && window.atob && window.ArrayBuffer && window.Uint8Array; if (!canProceed) { return null; } if (dataURI.split(',')[0].indexOf('base64') >= 0) { byteStr = atob(dataURI.split(',')[1]); } else { byteStr = decodeURIComponent(dataURI.split(',')[1]); } arrayBuffer = new ArrayBuffer(byteStr.length); intArray = new Uint8Array(arrayBuffer); for (i = 0; i < byteStr.length; i += 1) { intArray[i] = byteStr.charCodeAt(i); } mimeStr = dataURI.split(',')[0].split(':')[1].split(';')[0]; if (canBlob) { return new Blob([$h.hasArrayBufferViewSupport() ? intArray : arrayBuffer], {type: mimeStr}); } bb = new BlobBuilder(); bb.append(arrayBuffer); return bb.getBlob(mimeStr); }, arrayBuffer2String: function (buffer) { //noinspection JSUnresolvedVariable if (window.TextDecoder) { // noinspection JSUnresolvedFunction return new TextDecoder("utf-8").decode(buffer); } var array = Array.prototype.slice.apply(new Uint8Array(buffer)), out = '', i = 0, len, c, char2, char3; len = array.length; while (i < len) { c = array[i++]; switch (c >> 4) { // jshint ignore:line case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: // 0xxxxxxx out += String.fromCharCode(c); break; case 12: case 13: // 110x xxxx 10xx xxxx char2 = array[i++]; out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); // jshint ignore:line break; case 14: // 1110 xxxx 10xx xxxx 10xx xxxx char2 = array[i++]; char3 = array[i++]; out += String.fromCharCode(((c & 0x0F) << 12) | // jshint ignore:line ((char2 & 0x3F) << 6) | // jshint ignore:line ((char3 & 0x3F) << 0)); // jshint ignore:line break; } } return out; }, isHtml: function (str) { var a = document.createElement('div'); a.innerHTML = str; for (var c = a.childNodes, i = c.length; i--;) { if (c[i].nodeType === 1) { return true; } } return false; }, isSvg: function (str) { return str.match(/^\s*<\?xml/i) && (str.match(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, replaceTags: function (str, tags) { var out = str; if (!tags) { return out; } $.each(tags, function (key, value) { if (typeof value === "function") { value = value(); } out = out.split(key).join(value); }); return out; }, cleanMemory: function ($thumb) { var data = $thumb.is('img') ? $thumb.attr('src') : $thumb.find('source').attr('src'); /** @namespace $h.objUrl.revokeObjectURL */ $h.objUrl.revokeObjectURL(data); }, findFileName: function (filePath) { var sepIndex = filePath.lastIndexOf('/'); if (sepIndex === -1) { sepIndex = filePath.lastIndexOf('\\'); } return filePath.split(filePath.substring(sepIndex, sepIndex + 1)).pop(); }, checkFullScreen: function () { //noinspection JSUnresolvedVariable return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; }, toggleFullScreen: function (maximize) { var doc = document, de = doc.documentElement; if (de && maximize && !$h.checkFullScreen()) { /** @namespace document.requestFullscreen */ /** @namespace document.msRequestFullscreen */ /** @namespace document.mozRequestFullScreen */ /** @namespace document.webkitRequestFullscreen */ /** @namespace Element.ALLOW_KEYBOARD_INPUT */ if (de.requestFullscreen) { de.requestFullscreen(); } else if (de.msRequestFullscreen) { de.msRequestFullscreen(); } else if (de.mozRequestFullScreen) { de.mozRequestFullScreen(); } else if (de.webkitRequestFullscreen) { de.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } } else { /** @namespace document.exitFullscreen */ /** @namespace document.msExitFullscreen */ /** @namespace document.mozCancelFullScreen */ /** @namespace document.webkitExitFullscreen */ if (doc.exitFullscreen) { doc.exitFullscreen(); } else if (doc.msExitFullscreen) { doc.msExitFullscreen(); } else if (doc.mozCancelFullScreen) { doc.mozCancelFullScreen(); } else if (doc.webkitExitFullscreen) { doc.webkitExitFullscreen(); } } }, moveArray: function (arr, oldIndex, newIndex) { if (newIndex >= arr.length) { var k = newIndex - arr.length; while ((k--) + 1) { arr.push(undefined); } } arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]); return arr; }, cleanZoomCache: function ($el) { var $cache = $el.closest('.kv-zoom-cache-theme'); if (!$cache.length) { $cache = $el.closest('.kv-zoom-cache'); } $cache.remove(); }, setOrientation: function (buffer, callback) { var scanner = new DataView(buffer), idx = 0, value = 1, // Non-rotated is the default maxBytes, uInt16, exifLength; if (scanner.getUint16(idx) !== 0xFFD8 || buffer.length < 2) { // not a proper JPEG if (callback) { callback(); } return; } idx += 2; maxBytes = scanner.byteLength; while (idx < maxBytes - 2) { uInt16 = scanner.getUint16(idx); idx += 2; switch (uInt16) { case 0xFFE1: // Start of EXIF exifLength = scanner.getUint16(idx); maxBytes = exifLength - idx; idx += 2; break; case 0x0112: // Orientation tag value = scanner.getUint16(idx + 6, false); maxBytes = 0; // Stop scanning break; } } if (callback) { callback(value); } }, validateOrientation: function (file, callback) { if (!window.FileReader || !window.DataView) { return; // skip orientation if pre-requisite libraries not supported by browser } var reader = new FileReader(), buffer; reader.onloadend = function () { buffer = reader.result; $h.setOrientation(buffer, callback); }; reader.readAsArrayBuffer(file); }, adjustOrientedImage: function ($img, isZoom) { var offsetContTop, offsetTop, newTop; if (!$img.hasClass('is-portrait-gt4')) { return; } if (isZoom) { $img.css({width: $img.parent().height()}); return; } else { $img.css({height: 'auto', width: $img.height()}); } offsetContTop = $img.parent().offset().top; offsetTop = $img.offset().top; newTop = offsetContTop - offsetTop; $img.css('margin-top', newTop); }, closeButton: function (css) { css = css ? 'close ' + css : 'close'; return ''; } }; FileInput = function (element, options) { var self = this; self.$element = $(element); self.$parent = self.$element.parent(); if (!self._validate()) { return; } self.isPreviewable = $h.hasFileAPISupport(); self.isIE9 = $h.isIE(9); self.isIE10 = $h.isIE(10); if (self.isPreviewable || self.isIE9) { self._init(options); self._listen(); } self.$element.removeClass('file-loading'); }; //noinspection JSUnusedGlobalSymbols FileInput.prototype = { constructor: FileInput, _cleanup: function () { var self = this; self.reader = null; self.formdata = {}; self.uploadCount = 0; self.uploadStatus = {}; self.uploadLog = []; self.uploadAsyncCount = 0; self.loadedImages = []; self.totalImagesCount = 0; self.ajaxRequests = []; self.clearStack(); self.fileInputCleared = false; self.fileBatchCompleted = true; if (!self.isPreviewable) { self.showPreview = false; } self.isError = false; self.ajaxAborted = false; self.cancelling = false; }, _init: function (options, refreshMode) { var self = this, f, $el = self.$element, $cont, t, tmp; self.options = options; $.each(options, function (key, value) { switch (key) { case 'minFileCount': case 'maxFileCount': case 'minFileSize': case 'maxFileSize': case 'maxFilePreviewSize': case 'resizeImageQuality': case 'resizeIfSizeMoreThan': case 'progressUploadThreshold': case 'initialPreviewCount': case 'zoomModalHeight': case 'minImageHeight': case 'maxImageHeight': case 'minImageWidth': case 'maxImageWidth': self[key] = $h.getNum(value); break; default: self[key] = value; break; } }); if (self.rtl) { // swap buttons for rtl tmp = self.previewZoomButtonIcons.prev; self.previewZoomButtonIcons.prev = self.previewZoomButtonIcons.next; self.previewZoomButtonIcons.next = tmp; } if (!refreshMode) { self._cleanup(); } self.$form = $el.closest('form'); self._initTemplateDefaults(); self.uploadFileAttr = !$h.isEmpty($el.attr('name')) ? $el.attr('name') : 'file_data'; t = self._getLayoutTemplate('progress'); self.progressTemplate = t.replace('{class}', self.progressClass); self.progressCompleteTemplate = t.replace('{class}', self.progressCompleteClass); self.progressErrorTemplate = t.replace('{class}', self.progressErrorClass); self.dropZoneEnabled = $h.hasDragDropSupport() && self.dropZoneEnabled; self.isDisabled = $el.attr('disabled') || $el.attr('readonly'); if (self.isDisabled) { $el.attr('disabled', true); } self.isAjaxUpload = $h.hasFileUploadSupport() && !$h.isEmpty(self.uploadUrl); self.isClickable = self.browseOnZoneClick && self.showPreview && (self.isAjaxUpload && self.dropZoneEnabled || !$h.isEmpty(self.defaultPreviewContent)); self.slug = typeof options.slugCallback === "function" ? options.slugCallback : self._slugDefault; self.mainTemplate = self.showCaption ? self._getLayoutTemplate('main1') : self._getLayoutTemplate('main2'); self.captionTemplate = self._getLayoutTemplate('caption'); self.previewGenericTemplate = self._getPreviewTemplate('generic'); if (!self.imageCanvas && self.resizeImage && (self.maxImageWidth || self.maxImageHeight)) { self.imageCanvas = document.createElement('canvas'); self.imageCanvasContext = self.imageCanvas.getContext('2d'); } if ($h.isEmpty($el.attr('id'))) { $el.attr('id', $h.uniqId()); } self.namespace = '.fileinput_' + $el.attr('id').replace(/-/g, '_'); if (self.$container === undefined) { self.$container = self._createContainer(); } else { self._refreshContainer(); } $cont = self.$container; self.$dropZone = $cont.find('.file-drop-zone'); self.$progress = $cont.find('.kv-upload-progress'); self.$btnUpload = $cont.find('.fileinput-upload'); self.$captionContainer = $h.getElement(options, 'elCaptionContainer', $cont.find('.file-caption')); self.$caption = $h.getElement(options, 'elCaptionText', $cont.find('.file-caption-name')); if (!$h.isEmpty(self.msgPlaceholder)) { f = $el.attr('multiple') ? self.filePlural : self.fileSingle; self.$caption.attr('placeholder', self.msgPlaceholder.replace('{files}', f)); } self.$captionIcon = self.$captionContainer.find('.file-caption-icon'); self.$previewContainer = $h.getElement(options, 'elPreviewContainer', $cont.find('.file-preview')); self.$preview = $h.getElement(options, 'elPreviewImage', $cont.find('.file-preview-thumbnails')); self.$previewStatus = $h.getElement(options, 'elPreviewStatus', $cont.find('.file-preview-status')); self.$errorContainer = $h.getElement(options, 'elErrorContainer', self.$previewContainer.find('.kv-fileinput-error')); self._validateDisabled(); if (!$h.isEmpty(self.msgErrorClass)) { $h.addCss(self.$errorContainer, self.msgErrorClass); } if (!refreshMode) { self.$errorContainer.hide(); self.previewInitId = "preview-" + $h.uniqId(); self._initPreviewCache(); self._initPreview(true); self._initPreviewActions(); self._setFileDropZoneTitle(); if (self.$parent.hasClass('file-loading')) { self.$container.insertBefore(self.$parent); self.$parent.remove(); } } if ($el.attr('disabled')) { self.disable(); } self._initZoom(); if (self.hideThumbnailContent) { $h.addCss(self.$preview, 'hide-content'); } }, _initTemplateDefaults: function () { var self = this, tMain1, tMain2, tPreview, tFileIcon, tClose, tCaption, tBtnDefault, tBtnLink, tBtnBrowse, tModalMain, tModal, tProgress, tSize, tFooter, tActions, tActionDelete, tActionUpload, tActionDownload, tActionZoom, tActionDrag, tIndicator, tTagBef, tTagBef1, tTagBef2, tTagAft, tGeneric, tHtml, tImage, tText, tOffice, tVideo, tAudio, tFlash, tObject, tPdf, tOther, tZoomCache, vDefaultDim; tMain1 = '{preview}\n' + '
\n' + '
\n' + ' {caption}\n' + '
\n' + ' {remove}\n' + ' {cancel}\n' + ' {upload}\n' + ' {browse}\n' + '
\n' + '
'; tMain2 = '{preview}\n
\n
\n{remove}\n{cancel}\n{upload}\n{browse}\n'; tPreview = '
\n' + ' {close}' + '
\n' + '
\n' + '
\n' + '
' + '
\n' + '
\n' + '
\n' + '
'; tClose = $h.closeButton('fileinput-remove'); tFileIcon = ''; tCaption = '
\n' + ' \n' + ' \n' + '
'; //noinspection HtmlUnknownAttribute tBtnDefault = ''; //noinspection HtmlUnknownAttribute tBtnLink = '{icon} {label}'; //noinspection HtmlUnknownAttribute tBtnBrowse = '
{icon} {label}
'; tModalMain = ''; tModal = '\n'; tProgress = '
\n' + '
\n' + ' {status}\n' + '
\n' + '
'; tSize = ' ({sizeText})'; tFooter = ''; tActions = '
\n' + ' \n' + '
\n' + '{drag}\n' + '
'; //noinspection HtmlUnknownAttribute tActionDelete = '\n'; tActionUpload = ''; tActionDownload = '{downloadIcon}'; tActionZoom = ''; tActionDrag = '{dragIcon}'; tIndicator = '
{indicator}
'; tTagBef = '
\n'; tTagBef2 = tTagBef + ' title="{caption}">
\n'; tTagAft = '
{footer}\n
\n'; tGeneric = '{content}\n'; tHtml = '
{data}
\n'; tImage = '\n'; tText = '\n'; tOffice = ''; tVideo = '\n'; tAudio = '\n'; tFlash = '\n'; tPdf = '\n'; tObject = '\n' + '\n' + $h.OBJECT_PARAMS + ' ' + $h.DEFAULT_PREVIEW + '\n\n'; tOther = '
\n' + $h.DEFAULT_PREVIEW + '\n
\n'; tZoomCache = ''; vDefaultDim = {width: "100%", height: "100%", 'min-height': "480px"}; self.defaults = { layoutTemplates: { main1: tMain1, main2: tMain2, preview: tPreview, close: tClose, fileIcon: tFileIcon, caption: tCaption, modalMain: tModalMain, modal: tModal, progress: tProgress, size: tSize, footer: tFooter, indicator: tIndicator, actions: tActions, actionDelete: tActionDelete, actionUpload: tActionUpload, actionDownload: tActionDownload, actionZoom: tActionZoom, actionDrag: tActionDrag, btnDefault: tBtnDefault, btnLink: tBtnLink, btnBrowse: tBtnBrowse, zoomCache: tZoomCache }, previewMarkupTags: { tagBefore1: tTagBef1, tagBefore2: tTagBef2, tagAfter: tTagAft }, previewContentTemplates: { generic: tGeneric, html: tHtml, image: tImage, text: tText, office: tOffice, video: tVideo, audio: tAudio, flash: tFlash, object: tObject, pdf: tPdf, other: tOther }, allowedPreviewTypes: ['image', 'html', 'text', 'video', 'audio', 'flash', 'pdf', 'object'], previewTemplates: {}, previewSettings: { image: {width: "auto", height: "auto", 'max-width': "100%", 'max-height': "100%"}, html: {width: "213px", height: "160px"}, text: {width: "213px", height: "160px"}, office: {width: "213px", height: "160px"}, video: {width: "213px", height: "160px"}, audio: {width: "100%", height: "30px"}, flash: {width: "213px", height: "160px"}, object: {width: "213px", height: "160px"}, pdf: {width: "213px", height: "160px"}, other: {width: "213px", height: "160px"} }, previewSettingsSmall: { image: {width: "auto", height: "auto", 'max-width': "100%", 'max-height': "100%"}, html: {width: "100%", height: "160px"}, text: {width: "100%", height: "160px"}, office: {width: "100%", height: "160px"}, video: {width: "100%", height: "auto"}, audio: {width: "100%", height: "30px"}, flash: {width: "100%", height: "auto"}, object: {width: "100%", height: "auto"}, pdf: {width: "100%", height: "160px"}, other: {width: "100%", height: "160px"} }, previewZoomSettings: { image: {width: "auto", height: "auto", 'max-width': "100%", 'max-height': "100%"}, html: vDefaultDim, text: vDefaultDim, office: {width: "100%", height: "100%", 'max-width': "100%", 'min-height': "480px"}, video: {width: "auto", height: "100%", 'max-width': "100%"}, audio: {width: "100%", height: "30px"}, flash: {width: "auto", height: "480px"}, object: {width: "auto", height: "100%", 'max-width': "100%", 'min-height': "480px"}, pdf: vDefaultDim, other: {width: "auto", height: "100%", 'min-height': "480px"} }, fileTypeSettings: { image: function (vType, vName) { return ($h.compare(vType, 'image.*') && !$h.compare(vType, /(tiff?|wmf)$/i) || $h.compare(vName, /\.(gif|png|jpe?g)$/i)); }, html: function (vType, vName) { return $h.compare(vType, 'text/html') || $h.compare(vName, /\.(htm|html)$/i); }, office: function (vType, vName) { return $h.compare(vType, /(word|excel|powerpoint|office|iwork-pages|tiff?)$/i) || $h.compare(vName, /\.(rtf|docx?|xlsx?|pptx?|pps|potx?|ods|odt|pages|ai|dxf|ttf|tiff?|wmf|e?ps)$/i); }, text: function (vType, vName) { return $h.compare(vType, 'text.*') || $h.compare(vName, /\.(xml|javascript)$/i) || $h.compare(vName, /\.(txt|md|csv|nfo|ini|json|php|js|css)$/i); }, video: function (vType, vName) { return $h.compare(vType, 'video.*') && ($h.compare(vType, /(ogg|mp4|mp?g|mov|webm|3gp)$/i) || $h.compare(vName, /\.(og?|mp4|webm|mp?g|mov|3gp)$/i)); }, audio: function (vType, vName) { return $h.compare(vType, 'audio.*') && ($h.compare(vName, /(ogg|mp3|mp?g|wav)$/i) || $h.compare(vName, /\.(og?|mp3|mp?g|wav)$/i)); }, flash: function (vType, vName) { return $h.compare(vType, 'application/x-shockwave-flash', true) || $h.compare(vName, /\.(swf)$/i); }, pdf: function (vType, vName) { return $h.compare(vType, 'application/pdf', true) || $h.compare(vName, /\.(pdf)$/i); }, object: function () { return true; }, other: function () { return true; } }, fileActionSettings: { showRemove: true, showUpload: true, showDownload: true, showZoom: true, showDrag: true, removeIcon: '', removeClass: 'btn btn-kv btn-default btn-outline-secondary', removeErrorClass: 'btn btn-kv btn-danger', removeTitle: 'Remove file', uploadIcon: '', uploadClass: 'btn btn-kv btn-default btn-outline-secondary', uploadTitle: 'Upload file', uploadRetryIcon: '', uploadRetryTitle: 'Retry upload', downloadIcon: '', downloadClass: 'btn btn-kv btn-default btn-outline-secondary', downloadTitle: 'Download file', zoomIcon: '', zoomClass: 'btn btn-kv btn-default btn-outline-secondary', zoomTitle: 'View Details', dragIcon: '', dragClass: 'text-info', dragTitle: 'Move / Rearrange', dragSettings: {}, indicatorNew: '', indicatorSuccess: '', indicatorError: '', indicatorLoading: '', indicatorNewTitle: 'Not uploaded yet', indicatorSuccessTitle: 'Uploaded', indicatorErrorTitle: 'Upload Error', indicatorLoadingTitle: 'Uploading ...' } }; $.each(self.defaults, function (key, setting) { if (key === 'allowedPreviewTypes') { if (self.allowedPreviewTypes === undefined) { self.allowedPreviewTypes = setting; } return; } self[key] = $.extend(true, {}, setting, self[key]); }); self._initPreviewTemplates(); }, _initPreviewTemplates: function () { var self = this, cfg = self.defaults, tags = self.previewMarkupTags, tagBef, tagAft = tags.tagAfter; $.each(cfg.previewContentTemplates, function (key, value) { if ($h.isEmpty(self.previewTemplates[key])) { tagBef = tags.tagBefore2; if (key === 'generic' || key === 'image' || key === 'html' || key === 'text') { tagBef = tags.tagBefore1; } self.previewTemplates[key] = tagBef + value + tagAft; } }); }, _initPreviewCache: function () { var self = this; self.previewCache = { data: {}, init: function () { var content = self.initialPreview; if (content.length > 0 && !$h.isArray(content)) { content = content.split(self.initialPreviewDelimiter); } self.previewCache.data = { content: content, config: self.initialPreviewConfig, tags: self.initialPreviewThumbTags }; }, count: function () { return !!self.previewCache.data && !!self.previewCache.data.content ? self.previewCache.data.content.length : 0; }, get: function (i, isDisabled) { var ind = 'init_' + i, data = self.previewCache.data, config = data.config[i], content = data.content[i], previewId = self.previewInitId + '-' + ind, out, $tmp, cat, ftr, fname, ftype, frameClass, asData = $h.ifSet('previewAsData', config, self.initialPreviewAsData), parseTemplate = function (cat, dat, fn, ft, id, ftr, ind, fc, t) { fc = ' file-preview-initial ' + $h.SORT_CSS + (fc ? ' ' + fc : ''); return self._generatePreviewTemplate(cat, dat, fn, ft, id, false, null, fc, ftr, ind, t); }; if (!content) { return ''; } isDisabled = isDisabled === undefined ? true : isDisabled; cat = $h.ifSet('type', config, self.initialPreviewFileType || 'generic'); fname = $h.ifSet('filename', config, $h.ifSet('caption', config)); ftype = $h.ifSet('filetype', config, cat); ftr = self.previewCache.footer(i, isDisabled, (config && config.size || null)); frameClass = $h.ifSet('frameClass', config); if (asData) { out = parseTemplate(cat, content, fname, ftype, previewId, ftr, ind, frameClass); } else { out = parseTemplate('generic', content, fname, ftype, previewId, ftr, ind, frameClass, cat) .setTokens({'content': data.content[i]}); } if (data.tags.length && data.tags[i]) { out = $h.replaceTags(out, data.tags[i]); } /** @namespace config.frameAttr */ if (!$h.isEmpty(config) && !$h.isEmpty(config.frameAttr)) { $tmp = $(document.createElement('div')).html(out); $tmp.find('.file-preview-initial').attr(config.frameAttr); out = $tmp.html(); $tmp.remove(); } return out; }, add: function (content, config, tags, append) { var data = self.previewCache.data, index; if (!$h.isArray(content)) { content = content.split(self.initialPreviewDelimiter); } if (append) { index = data.content.push(content) - 1; data.config[index] = config; data.tags[index] = tags; } else { index = content.length - 1; data.content = content; data.config = config; data.tags = tags; } self.previewCache.data = data; return index; }, set: function (content, config, tags, append) { var data = self.previewCache.data, i, chk; if (!content || !content.length) { return; } if (!$h.isArray(content)) { content = content.split(self.initialPreviewDelimiter); } chk = content.filter(function (n) { return n !== null; }); if (!chk.length) { return; } if (data.content === undefined) { data.content = []; } if (data.config === undefined) { data.config = []; } if (data.tags === undefined) { data.tags = []; } if (append) { for (i = 0; i < content.length; i++) { if (content[i]) { data.content.push(content[i]); } } for (i = 0; i < config.length; i++) { if (config[i]) { data.config.push(config[i]); } } for (i = 0; i < tags.length; i++) { if (tags[i]) { data.tags.push(tags[i]); } } } else { data.content = content; data.config = config; data.tags = tags; } self.previewCache.data = data; }, unset: function (index) { var chk = self.previewCache.count(); if (!chk) { return; } if (chk === 1) { self.previewCache.data.content = []; self.previewCache.data.config = []; self.previewCache.data.tags = []; self.initialPreview = []; self.initialPreviewConfig = []; self.initialPreviewThumbTags = []; return; } self.previewCache.data.content = $h.spliceArray(self.previewCache.data.content, index); self.previewCache.data.config = $h.spliceArray(self.previewCache.data.config, index); self.previewCache.data.tags = $h.spliceArray(self.previewCache.data.tags, index); }, out: function () { var html = '', caption, len = self.previewCache.count(), i; if (len === 0) { return {content: '', caption: ''}; } for (i = 0; i < len; i++) { html += self.previewCache.get(i); } caption = self._getMsgSelected(len); return {content: html, caption: caption}; }, footer: function (i, isDisabled, size) { var data = self.previewCache.data || {}; if ($h.isEmpty(data.content)) { return ''; } if ($h.isEmpty(data.config) || $h.isEmpty(data.config[i])) { data.config[i] = {}; } isDisabled = isDisabled === undefined ? true : isDisabled; var config = data.config[i], caption = $h.ifSet('caption', config), a, width = $h.ifSet('width', config, 'auto'), url = $h.ifSet('url', config, false), key = $h.ifSet('key', config, null), fs = self.fileActionSettings, initPreviewShowDel = self.initialPreviewShowDelete || false, dUrl = config.downloadUrl || self.initialPreviewDownloadUrl || '', dFil = config.filename || config.caption || '', initPreviewShowDwl = !!(dUrl), sDel = $h.ifSet('showDelete', config, $h.ifSet('showDelete', fs, initPreviewShowDel)), sDwl = $h.ifSet('showDownload', config, $h.ifSet('showDownload', fs, initPreviewShowDwl)), sZm = $h.ifSet('showZoom', config, $h.ifSet('showZoom', fs, true)), sDrg = $h.ifSet('showDrag', config, $h.ifSet('showDrag', fs, true)), dis = (url === false) && isDisabled; sDwl = sDwl && config.downloadUrl !== false && !!dUrl; a = self._renderFileActions(false, sDwl, sDel, sZm, sDrg, dis, url, key, true, dUrl, dFil); return self._getLayoutTemplate('footer').setTokens({ 'progress': self._renderThumbProgress(), 'actions': a, 'caption': caption, 'size': self._getSize(size), 'width': width, 'indicator': '' }); } }; self.previewCache.init(); }, _handler: function ($el, event, callback) { var self = this, ns = self.namespace, ev = event.split(' ').join(ns + ' ') + ns; if (!$el || !$el.length) { return; } $el.off(ev).on(ev, callback); }, _log: function (msg) { var self = this, id = self.$element.attr('id'); if (id) { msg = '"' + id + '": ' + msg; } if (typeof window.console.log !== "undefined") { window.console.log(msg); } else { window.alert(msg); } }, _validate: function () { var self = this, status = self.$element.attr('type') === 'file'; if (!status) { self._log('The input "type" must be set to "file" for initializing the "bootstrap-fileinput" plugin.'); } return status; }, _errorsExist: function () { var self = this, $err, $errList = self.$errorContainer.find('li'); if ($errList.length) { return true; } $err = $(document.createElement('div')).html(self.$errorContainer.html()); $err.find('.kv-error-close').remove(); $err.find('ul').remove(); return !!$.trim($err.text()).length; }, _errorHandler: function (evt, caption) { var self = this, err = evt.target.error, showError = function (msg) { self._showError(msg.replace('{name}', caption)); }; /** @namespace err.NOT_FOUND_ERR */ /** @namespace err.SECURITY_ERR */ /** @namespace err.NOT_READABLE_ERR */ if (err.code === err.NOT_FOUND_ERR) { showError(self.msgFileNotFound); } else if (err.code === err.SECURITY_ERR) { showError(self.msgFileSecured); } else if (err.code === err.NOT_READABLE_ERR) { showError(self.msgFileNotReadable); } else if (err.code === err.ABORT_ERR) { showError(self.msgFilePreviewAborted); } else { showError(self.msgFilePreviewError); } }, _addError: function (msg) { var self = this, $error = self.$errorContainer; if (msg && $error.length) { $error.html(self.errorCloseButton + msg); self._handler($error.find('.kv-error-close'), 'click', function () { setTimeout(function() { if (self.showPreview && !self.getFrames().length) { self.clear(); } $error.fadeOut('slow'); }, 10); }); } }, _setValidationError: function (css) { var self = this; css = (css ? css + ' ' : '') + 'has-error'; self.$container.removeClass(css).addClass('has-error'); $h.addCss(self.$captionContainer, 'is-invalid'); }, _resetErrors: function (fade) { var self = this, $error = self.$errorContainer; self.isError = false; self.$container.removeClass('has-error'); self.$captionContainer.removeClass('is-invalid'); $error.html(''); if (fade) { $error.fadeOut('slow'); } else { $error.hide(); } }, _showFolderError: function (folders) { var self = this, $error = self.$errorContainer, msg; if (!folders) { return; } msg = self.msgFoldersNotAllowed.replace('{n}', folders); self._addError(msg); self._setValidationError(); $error.fadeIn(800); self._raise('filefoldererror', [folders, msg]); }, _showUploadError: function (msg, params, event) { var self = this, $error = self.$errorContainer, ev = event || 'fileuploaderror', e = params && params.id ? '
  • ' + msg + '
  • ' : '
  • ' + msg + '
  • '; if ($error.find('ul').length === 0) { self._addError('
      ' + e + '
    '); } else { $error.find('ul').append(e); } $error.fadeIn(800); self._raise(ev, [params, msg]); self._setValidationError('file-input-new'); return true; }, _showError: function (msg, params, event) { var self = this, $error = self.$errorContainer, ev = event || 'fileerror'; params = params || {}; params.reader = self.reader; self._addError(msg); $error.fadeIn(800); self._raise(ev, [params, msg]); if (!self.isAjaxUpload) { self._clearFileInput(); } self._setValidationError('file-input-new'); self.$btnUpload.attr('disabled', true); return true; }, _noFilesError: function (params) { var self = this, label = self.minFileCount > 1 ? self.filePlural : self.fileSingle, msg = self.msgFilesTooLess.replace('{n}', self.minFileCount).replace('{files}', label), $error = self.$errorContainer; self._addError(msg); self.isError = true; self._updateFileDetails(0); $error.fadeIn(800); self._raise('fileerror', [params, msg]); self._clearFileInput(); self._setValidationError(); }, _parseError: function (operation, jqXHR, errorThrown, fileName) { /** @namespace jqXHR.responseJSON */ var self = this, errMsg = $.trim(errorThrown + ''), textPre, text = jqXHR.responseJSON !== undefined && jqXHR.responseJSON.error !== undefined ? jqXHR.responseJSON.error : jqXHR.responseText; if (self.cancelling && self.msgUploadAborted) { errMsg = self.msgUploadAborted; } if (self.showAjaxErrorDetails && text) { text = $.trim(text.replace(/\n\s*\n/g, '\n')); textPre = text.length ? '
    ' + text + '
    ' : ''; errMsg += errMsg ? textPre : text; } if (!errMsg) { errMsg = self.msgAjaxError.replace('{operation}', operation); } self.cancelling = false; return fileName ? '' + fileName + ': ' + errMsg : errMsg; }, _parseFileType: function (type, name) { var self = this, isValid, vType, cat, i, types = self.allowedPreviewTypes || []; if (type === 'application/text-plain') { return 'text'; } for (i = 0; i < types.length; i++) { cat = types[i]; isValid = self.fileTypeSettings[cat]; vType = isValid(type, name) ? cat : ''; if (!$h.isEmpty(vType)) { return vType; } } return 'other'; }, _getPreviewIcon: function (fname) { var self = this, ext, out = null; if (fname && fname.indexOf('.') > -1) { ext = fname.split('.').pop(); if (self.previewFileIconSettings) { out = self.previewFileIconSettings[ext] || self.previewFileIconSettings[ext.toLowerCase()] || null; } if (self.previewFileExtSettings) { $.each(self.previewFileExtSettings, function (key, func) { if (self.previewFileIconSettings[key] && func(ext)) { out = self.previewFileIconSettings[key]; //noinspection UnnecessaryReturnStatementJS return; } }); } } return out; }, _parseFilePreviewIcon: function (content, fname) { var self = this, icn = self._getPreviewIcon(fname) || self.previewFileIcon, out = content; if (out.indexOf('{previewFileIcon}') > -1) { out = out.setTokens({'previewFileIconClass': self.previewFileIconClass, 'previewFileIcon': icn}); } return out; }, _raise: function (event, params) { var self = this, e = $.Event(event); if (params !== undefined) { self.$element.trigger(e, params); } else { self.$element.trigger(e); } if (e.isDefaultPrevented() || e.result === false) { return false; } switch (event) { // ignore these events case 'filebatchuploadcomplete': case 'filebatchuploadsuccess': case 'fileuploaded': case 'fileclear': case 'filecleared': case 'filereset': case 'fileerror': case 'filefoldererror': case 'fileuploaderror': case 'filebatchuploaderror': case 'filedeleteerror': case 'filecustomerror': case 'filesuccessremove': break; // receive data response via `filecustomerror` event` default: if (!self.ajaxAborted) { self.ajaxAborted = e.result; } break; } return true; }, _listenFullScreen: function (isFullScreen) { var self = this, $modal = self.$modal, $btnFull, $btnBord; if (!$modal || !$modal.length) { return; } $btnFull = $modal && $modal.find('.btn-fullscreen'); $btnBord = $modal && $modal.find('.btn-borderless'); if (!$btnFull.length || !$btnBord.length) { return; } $btnFull.removeClass('active').attr('aria-pressed', 'false'); $btnBord.removeClass('active').attr('aria-pressed', 'false'); if (isFullScreen) { $btnFull.addClass('active').attr('aria-pressed', 'true'); } else { $btnBord.addClass('active').attr('aria-pressed', 'true'); } if ($modal.hasClass('file-zoom-fullscreen')) { self._maximizeZoomDialog(); } else { if (isFullScreen) { self._maximizeZoomDialog(); } else { $btnBord.removeClass('active').attr('aria-pressed', 'false'); } } }, _listen: function () { var self = this, $el = self.$element, $form = self.$form, $cont = self.$container, fullScreenEvents; self._handler($el, 'change', $.proxy(self._change, self)); if (self.showBrowse) { self._handler(self.$btnFile, 'click', $.proxy(self._browse, self)); } self._handler($cont.find('.fileinput-remove:not([disabled])'), 'click', $.proxy(self.clear, self)); self._handler($cont.find('.fileinput-cancel'), 'click', $.proxy(self.cancel, self)); self._initDragDrop(); self._handler($form, 'reset', $.proxy(self.clear, self)); if (!self.isAjaxUpload) { self._handler($form, 'submit', $.proxy(self._submitForm, self)); } self._handler(self.$container.find('.fileinput-upload'), 'click', $.proxy(self._uploadClick, self)); self._handler($(window), 'resize', function () { self._listenFullScreen(screen.width === window.innerWidth && screen.height === window.innerHeight); }); fullScreenEvents = 'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'; self._handler($(document), fullScreenEvents, function () { self._listenFullScreen($h.checkFullScreen()); }); self._autoFitContent(); self._initClickable(); }, _autoFitContent: function () { var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, self = this, config = width < 400 ? (self.previewSettingsSmall || self.defaults.previewSettingsSmall) : (self.previewSettings || self.defaults.previewSettings), sel; $.each(config, function (cat, settings) { sel = '.file-preview-frame .file-preview-' + cat; self.$preview.find(sel + '.kv-preview-data,' + sel + ' .kv-preview-data').css(settings); }); }, _initClickable: function () { var self = this, $zone; if (!self.isClickable) { return; } $zone = self.isAjaxUpload ? self.$dropZone : self.$preview.find('.file-default-preview'); $h.addCss($zone, 'clickable'); $zone.attr('tabindex', -1); self._handler($zone, 'click', function (e) { var $tar = $(e.target); if (!$zone.find('.kv-fileinput-error:visible').length && (!$tar.parents('.file-preview-thumbnails').length || $tar.parents('.file-default-preview').length)) { self.$element.trigger('click'); $zone.blur(); } }); }, _initDragDrop: function () { var self = this, $zone = self.$dropZone; if (self.isAjaxUpload && self.dropZoneEnabled && self.showPreview) { self._handler($zone, 'dragenter dragover', $.proxy(self._zoneDragEnter, self)); self._handler($zone, 'dragleave', $.proxy(self._zoneDragLeave, self)); self._handler($zone, 'drop', $.proxy(self._zoneDrop, self)); self._handler($(document), 'dragenter dragover drop', self._zoneDragDropInit); } }, _zoneDragDropInit: function (e) { e.stopPropagation(); e.preventDefault(); }, _zoneDragEnter: function (e) { var self = this, hasFiles = $.inArray('Files', e.originalEvent.dataTransfer.types) > -1; self._zoneDragDropInit(e); if (self.isDisabled || !hasFiles) { e.originalEvent.dataTransfer.effectAllowed = 'none'; e.originalEvent.dataTransfer.dropEffect = 'none'; return; } $h.addCss(self.$dropZone, 'file-highlighted'); }, _zoneDragLeave: function (e) { var self = this; self._zoneDragDropInit(e); if (self.isDisabled) { return; } self.$dropZone.removeClass('file-highlighted'); }, _zoneDrop: function (e) { var self = this; e.preventDefault(); /** @namespace e.originalEvent.dataTransfer */ if (self.isDisabled || $h.isEmpty(e.originalEvent.dataTransfer.files)) { return; } self._change(e, 'dragdrop'); self.$dropZone.removeClass('file-highlighted'); }, _uploadClick: function (e) { var self = this, $btn = self.$container.find('.fileinput-upload'), $form, isEnabled = !$btn.hasClass('disabled') && $h.isEmpty($btn.attr('disabled')); if (e && e.isDefaultPrevented()) { return; } if (!self.isAjaxUpload) { if (isEnabled && $btn.attr('type') !== 'submit') { $form = $btn.closest('form'); // downgrade to normal form submit if possible if ($form.length) { $form.trigger('submit'); } e.preventDefault(); } return; } e.preventDefault(); if (isEnabled) { self.upload(); } }, _submitForm: function () { var self = this; return self._isFileSelectionValid() && !self._abort({}); }, _clearPreview: function () { var self = this, $p = self.$preview, $thumbs = self.showUploadedThumbs ? self.getFrames(':not(.file-preview-success)') : self.getFrames(); $thumbs.each(function () { var $thumb = $(this); $thumb.remove(); $h.cleanZoomCache($p.find('#zoom-' + $thumb.attr('id'))); }); if (!self.getFrames().length || !self.showPreview) { self._resetUpload(); } self._validateDefaultPreview(); }, _initSortable: function () { var self = this, $el = self.$preview, settings, selector = '.' + $h.SORT_CSS; if (!window.KvSortable || $el.find(selector).length === 0) { return; } //noinspection JSUnusedGlobalSymbols settings = { handle: '.drag-handle-init', dataIdAttr: 'data-preview-id', scroll: false, draggable: selector, onSort: function (e) { var oldIndex = e.oldIndex, newIndex = e.newIndex, $frame, $dragEl, i = 0; self.initialPreview = $h.moveArray(self.initialPreview, oldIndex, newIndex); self.initialPreviewConfig = $h.moveArray(self.initialPreviewConfig, oldIndex, newIndex); self.previewCache.init(); self.getFrames('.file-preview-initial').each(function() { $(this).attr('data-fileindex', 'init_' + i); i++; }); self._raise('filesorted', { previewId: $(e.item).attr('id'), 'oldIndex': oldIndex, 'newIndex': newIndex, stack: self.initialPreviewConfig }); } }; if ($el.data('kvsortable')) { $el.kvsortable('destroy'); } $.extend(true, settings, self.fileActionSettings.dragSettings); $el.kvsortable(settings); }, _setPreviewContent: function (content) { var self = this; self.$preview.html(content); self._autoFitContent(); }, _initPreview: function (isInit) { var self = this, cap = self.initialCaption || '', out; if (!self.previewCache.count()) { self._clearPreview(); if (isInit) { self._setCaption(cap); } else { self._initCaption(); } return; } out = self.previewCache.out(); cap = isInit && self.initialCaption ? self.initialCaption : out.caption; self._setPreviewContent(out.content); self._setInitThumbAttr(); self._setCaption(cap); self._initSortable(); if (!$h.isEmpty(out.content)) { self.$container.removeClass('file-input-new'); } }, _getZoomButton: function (type) { var self = this, label = self.previewZoomButtonIcons[type], css = self.previewZoomButtonClasses[type], title = ' title="' + (self.previewZoomButtonTitles[type] || '') + '" ', params = title + (type === 'close' ? ' data-dismiss="modal" aria-hidden="true"' : ''); if (type === 'fullscreen' || type === 'borderless' || type === 'toggleheader') { params += ' data-toggle="button" aria-pressed="false" autocomplete="off"'; } return ''; }, _getModalContent: function () { var self = this; return self._getLayoutTemplate('modal').setTokens({ 'rtl': self.rtl ? ' kv-rtl' : '', 'zoomFrameClass': self.frameClass, 'heading': self.msgZoomModalHeading, 'prev': self._getZoomButton('prev'), 'next': self._getZoomButton('next'), 'toggleheader': self._getZoomButton('toggleheader'), 'fullscreen': self._getZoomButton('fullscreen'), 'borderless': self._getZoomButton('borderless'), 'close': self._getZoomButton('close') }); }, _listenModalEvent: function (event) { var self = this, $modal = self.$modal, getParams = function (e) { return { sourceEvent: e, previewId: $modal.data('previewId'), modal: $modal }; }; $modal.on(event + '.bs.modal', function (e) { var $btnFull = $modal.find('.btn-fullscreen'), $btnBord = $modal.find('.btn-borderless'); self._raise('filezoom' + event, getParams(e)); if (event === 'shown') { $btnBord.removeClass('active').attr('aria-pressed', 'false'); $btnFull.removeClass('active').attr('aria-pressed', 'false'); if ($modal.hasClass('file-zoom-fullscreen')) { self._maximizeZoomDialog(); if ($h.checkFullScreen()) { $btnFull.addClass('active').attr('aria-pressed', 'true'); } else { $btnBord.addClass('active').attr('aria-pressed', 'true'); } } } }); }, _initZoom: function () { var self = this, $dialog, modalMain = self._getLayoutTemplate('modalMain'), modalId = '#' + $h.MODAL_ID; if (!self.showPreview) { return; } self.$modal = $(modalId); if (!self.$modal || !self.$modal.length) { $dialog = $(document.createElement('div')).html(modalMain).insertAfter(self.$container); self.$modal = $(modalId).insertBefore($dialog); $dialog.remove(); } $h.initModal(self.$modal); self.$modal.html(self._getModalContent()); $.each($h.MODAL_EVENTS, function (key, event) { self._listenModalEvent(event); }); }, _initZoomButtons: function () { var self = this, previewId = self.$modal.data('previewId') || '', $first, $last, thumbs = self.getFrames().toArray(), len = thumbs.length, $prev = self.$modal.find('.btn-prev'), $next = self.$modal.find('.btn-next'); if (thumbs.length < 2) { $prev.hide(); $next.hide(); return; } else { $prev.show(); $next.show(); } if (!len) { return; } $first = $(thumbs[0]); $last = $(thumbs[len - 1]); $prev.removeAttr('disabled'); $next.removeAttr('disabled'); if ($first.length && $first.attr('id') === previewId) { $prev.attr('disabled', true); } if ($last.length && $last.attr('id') === previewId) { $next.attr('disabled', true); } }, _maximizeZoomDialog: function () { var self = this, $modal = self.$modal, $head = $modal.find('.modal-header:visible'), $foot = $modal.find('.modal-footer:visible'), $body = $modal.find('.modal-body'), h = $(window).height(), diff = 0; $modal.addClass('file-zoom-fullscreen'); if ($head && $head.length) { h -= $head.outerHeight(true); } if ($foot && $foot.length) { h -= $foot.outerHeight(true); } if ($body && $body.length) { diff = $body.outerHeight(true) - $body.height(); h -= diff; } $modal.find('.kv-zoom-body').height(h); }, _resizeZoomDialog: function (fullScreen) { var self = this, $modal = self.$modal, $btnFull = $modal.find('.btn-fullscreen'), $btnBord = $modal.find('.btn-borderless'); if ($modal.hasClass('file-zoom-fullscreen')) { $h.toggleFullScreen(false); if (!fullScreen) { if (!$btnFull.hasClass('active')) { $modal.removeClass('file-zoom-fullscreen'); self.$modal.find('.kv-zoom-body').css('height', self.zoomModalHeight); } else { $btnFull.removeClass('active').attr('aria-pressed', 'false'); } } else { if (!$btnFull.hasClass('active')) { $modal.removeClass('file-zoom-fullscreen'); self._resizeZoomDialog(true); if ($btnBord.hasClass('active')) { $btnBord.removeClass('active').attr('aria-pressed', 'false'); } } } } else { if (!fullScreen) { self._maximizeZoomDialog(); return; } $h.toggleFullScreen(true); } $modal.focus(); }, _setZoomContent: function ($frame, animate) { var self = this, $content, tmplt, body, title, $body, $dataEl, config, pid = $frame.attr('id'), $modal = self.$modal, $prev = $modal.find('.btn-prev'), $next = $modal.find('.btn-next'), $tmp, $btnFull = $modal.find('.btn-fullscreen'), $btnBord = $modal.find('.btn-borderless'), cap, size, $btnTogh = $modal.find('.btn-toggleheader'), $zoomPreview = self.$preview.find('#zoom-' + pid); tmplt = $zoomPreview.attr('data-template') || 'generic'; $content = $zoomPreview.find('.kv-file-content'); body = $content.length ? $content.html() : ''; cap = $frame.data('caption') || ''; size = $frame.data('size') || ''; title = cap + ' ' + size; $modal.find('.kv-zoom-title').attr('title', $('
    ').html(title).text()).html(title); $body = $modal.find('.kv-zoom-body'); $modal.removeClass('kv-single-content'); if (animate) { $tmp = $body.addClass('file-thumb-loading').clone().insertAfter($body); $body.html(body).hide(); $tmp.fadeOut('fast', function () { $body.fadeIn('fast', function () { $body.removeClass('file-thumb-loading'); }); $tmp.remove(); }); } else { $body.html(body); } config = self.previewZoomSettings[tmplt]; if (config) { $dataEl = $body.find('.kv-preview-data'); $h.addCss($dataEl, 'file-zoom-detail'); $.each(config, function (key, value) { $dataEl.css(key, value); if (($dataEl.attr('width') && key === 'width') || ($dataEl.attr('height') && key === 'height')) { $dataEl.removeAttr(key); } }); } $modal.data('previewId', pid); var $img = $body.find('img'); if ($img.length) { $h.adjustOrientedImage($img, true); } self._handler($prev, 'click', function () { self._zoomSlideShow('prev', pid); }); self._handler($next, 'click', function () { self._zoomSlideShow('next', pid); }); self._handler($btnFull, 'click', function () { self._resizeZoomDialog(true); }); self._handler($btnBord, 'click', function () { self._resizeZoomDialog(false); }); self._handler($btnTogh, 'click', function () { var $header = $modal.find('.modal-header'), $floatBar = $modal.find('.modal-body .floating-buttons'), ht, $actions = $header.find('.kv-zoom-actions'), resize = function (height) { var $body = self.$modal.find('.kv-zoom-body'), h = self.zoomModalHeight; if ($modal.hasClass('file-zoom-fullscreen')) { h = $body.outerHeight(true); if (!height) { h = h - $header.outerHeight(true); } } $body.css('height', height ? h + height : h); }; if ($header.is(':visible')) { ht = $header.outerHeight(true); $header.slideUp('slow', function () { $actions.find('.btn').appendTo($floatBar); resize(ht); }); } else { $floatBar.find('.btn').appendTo($actions); $header.slideDown('slow', function () { resize(); }); } $modal.focus(); }); self._handler($modal, 'keydown', function (e) { var key = e.which || e.keyCode; if (key === 37 && !$prev.attr('disabled')) { self._zoomSlideShow('prev', pid); } if (key === 39 && !$next.attr('disabled')) { self._zoomSlideShow('next', pid); } }); }, _zoomPreview: function ($btn) { var self = this, $frame, $modal = self.$modal; if (!$btn.length) { throw 'Cannot zoom to detailed preview!'; } $h.initModal($modal); $modal.html(self._getModalContent()); $frame = $btn.closest($h.FRAMES); self._setZoomContent($frame); $modal.modal('show'); self._initZoomButtons(); }, _zoomSlideShow: function (dir, previewId) { var self = this, $btn = self.$modal.find('.kv-zoom-actions .btn-' + dir), $targFrame, i, thumbs = self.getFrames().toArray(), len = thumbs.length, out; if ($btn.attr('disabled')) { return; } for (i = 0; i < len; i++) { if ($(thumbs[i]).attr('id') === previewId) { out = dir === 'prev' ? i - 1 : i + 1; break; } } if (out < 0 || out >= len || !thumbs[out]) { return; } $targFrame = $(thumbs[out]); if ($targFrame.length) { self._setZoomContent($targFrame, true); } self._initZoomButtons(); self._raise('filezoom' + dir, {'previewId': previewId, modal: self.$modal}); }, _initZoomButton: function () { var self = this; self.$preview.find('.kv-file-zoom').each(function () { var $el = $(this); self._handler($el, 'click', function () { self._zoomPreview($el); }); }); }, _clearObjects: function ($el) { $el.find('video audio').each(function () { this.pause(); $(this).remove(); }); $el.find('img object div').each(function () { $(this).remove(); }); }, _clearFileInput: function () { var self = this, $el = self.$element, $srcFrm, $tmpFrm, $tmpEl; self.fileInputCleared = true; if ($h.isEmpty($el.val())) { return; } // Fix for IE ver < 11, that does not clear file inputs. Requires a sequence of steps to prevent IE // crashing but still allow clearing of the file input. if (self.isIE9 || self.isIE10) { $srcFrm = $el.closest('form'); $tmpFrm = $(document.createElement('form')); $tmpEl = $(document.createElement('div')); $el.before($tmpEl); if ($srcFrm.length) { $srcFrm.after($tmpFrm); } else { $tmpEl.after($tmpFrm); } $tmpFrm.append($el).trigger('reset'); $tmpEl.before($el).remove(); $tmpFrm.remove(); } else { // normal input clear behavior for other sane browsers $el.val(''); } }, _resetUpload: function () { var self = this; self.uploadCache = {content: [], config: [], tags: [], append: true}; self.uploadCount = 0; self.uploadStatus = {}; self.uploadLog = []; self.uploadAsyncCount = 0; self.loadedImages = []; self.totalImagesCount = 0; self.$btnUpload.removeAttr('disabled'); self._setProgress(0); self.$progress.hide(); self._resetErrors(false); self.ajaxAborted = false; self.ajaxRequests = []; self._resetCanvas(); self.cacheInitialPreview = {}; if (self.overwriteInitial) { self.initialPreview = []; self.initialPreviewConfig = []; self.initialPreviewThumbTags = []; self.previewCache.data = { content: [], config: [], tags: [] }; } }, _resetCanvas: function () { var self = this; if (self.canvas && self.imageCanvasContext) { self.imageCanvasContext.clearRect(0, 0, self.canvas.width, self.canvas.height); } }, _hasInitialPreview: function () { var self = this; return !self.overwriteInitial && self.previewCache.count(); }, _resetPreview: function () { var self = this, out, cap; if (self.previewCache.count()) { out = self.previewCache.out(); self._setPreviewContent(out.content); self._setInitThumbAttr(); cap = self.initialCaption ? self.initialCaption : out.caption; self._setCaption(cap); } else { self._clearPreview(); self._initCaption(); } if (self.showPreview) { self._initZoom(); self._initSortable(); } }, _clearDefaultPreview: function () { var self = this; self.$preview.find('.file-default-preview').remove(); }, _validateDefaultPreview: function () { var self = this; if (!self.showPreview || $h.isEmpty(self.defaultPreviewContent)) { return; } self._setPreviewContent('
    ' + self.defaultPreviewContent + '
    '); self.$container.removeClass('file-input-new'); self._initClickable(); }, _resetPreviewThumbs: function (isAjax) { var self = this, out; if (isAjax) { self._clearPreview(); self.clearStack(); return; } if (self._hasInitialPreview()) { out = self.previewCache.out(); self._setPreviewContent(out.content); self._setInitThumbAttr(); self._setCaption(out.caption); self._initPreviewActions(); } else { self._clearPreview(); } }, _getLayoutTemplate: function (t) { var self = this, template = self.layoutTemplates[t]; if ($h.isEmpty(self.customLayoutTags)) { return template; } return $h.replaceTags(template, self.customLayoutTags); }, _getPreviewTemplate: function (t) { var self = this, template = self.previewTemplates[t]; if ($h.isEmpty(self.customPreviewTags)) { return template; } return $h.replaceTags(template, self.customPreviewTags); }, _getOutData: function (jqXHR, responseData, filesData) { var self = this; jqXHR = jqXHR || {}; responseData = responseData || {}; filesData = filesData || self.filestack.slice(0) || {}; return { form: self.formdata, files: filesData, filenames: self.filenames, filescount: self.getFilesCount(), extra: self._getExtraData(), response: responseData, reader: self.reader, jqXHR: jqXHR }; }, _getMsgSelected: function (n) { var self = this, strFiles = n === 1 ? self.fileSingle : self.filePlural; return n > 0 ? self.msgSelected.replace('{n}', n).replace('{files}', strFiles) : self.msgNoFilesSelected; }, _getFrame: function (id) { var self = this, $frame = $('#' + id); if (!$frame.length) { self._log('Invalid thumb frame with id: "' + id + '".'); return null; } return $frame; }, _getThumbs: function (css) { css = css || ''; return this.getFrames(':not(.file-preview-initial)' + css); }, _getExtraData: function (previewId, index) { var self = this, data = self.uploadExtraData; if (typeof self.uploadExtraData === "function") { data = self.uploadExtraData(previewId, index); } return data; }, _initXhr: function (xhrobj, previewId, fileCount) { var self = this; if (xhrobj.upload) { xhrobj.upload.addEventListener('progress', function (event) { var pct = 0, total = event.total, position = event.loaded || event.position; /** @namespace event.lengthComputable */ if (event.lengthComputable) { pct = Math.floor(position / total * 100); } if (previewId) { self._setAsyncUploadStatus(previewId, pct, fileCount); } else { self._setProgress(pct); } }, false); } return xhrobj; }, _mergeAjaxCallback: function (funcName, srcFunc, type) { var self = this, settings = self.ajaxSettings, flag = self.mergeAjaxCallbacks, targFunc; if (type === 'delete') { settings = self.ajaxDeleteSettings; flag = self.mergeAjaxDeleteCallbacks; } targFunc = settings[funcName]; if (flag && typeof targFunc === "function") { if (flag === 'before') { settings[funcName] = function () { targFunc.apply(this, arguments); srcFunc.apply(this, arguments); }; } else { settings[funcName] = function () { srcFunc.apply(this, arguments); targFunc.apply(this, arguments); }; } } else { settings[funcName] = srcFunc; } if (type === 'delete') { self.ajaxDeleteSettings = settings; } else { self.ajaxSettings = settings; } }, _ajaxSubmit: function (fnBefore, fnSuccess, fnComplete, fnError, previewId, index) { var self = this, settings; if (!self._raise('filepreajax', [previewId, index])) { return; } self._uploadExtra(previewId, index); self._mergeAjaxCallback('beforeSend', fnBefore); self._mergeAjaxCallback('success', fnSuccess); self._mergeAjaxCallback('complete', fnComplete); self._mergeAjaxCallback('error', fnError); settings = $.extend(true, {}, { xhr: function () { var xhrobj = $.ajaxSettings.xhr(); return self._initXhr(xhrobj, previewId, self.getFileStack().length); }, url: index && self.uploadUrlThumb ? self.uploadUrlThumb : self.uploadUrl, type: 'POST', dataType: 'json', data: self.formdata, cache: false, processData: false, contentType: false }, self.ajaxSettings); self.ajaxRequests.push($.ajax(settings)); }, _mergeArray: function (prop, content) { var self = this, arr1 = $h.cleanArray(self[prop]), arr2 = $h.cleanArray(content); self[prop] = arr1.concat(arr2); }, _initUploadSuccess: function (out, $thumb, allFiles) { var self = this, append, data, index, $div, $newCache, content, config, tags, i; if (!self.showPreview || typeof out !== 'object' || $.isEmptyObject(out)) { return; } if (out.initialPreview !== undefined && out.initialPreview.length > 0) { self.hasInitData = true; content = out.initialPreview || []; config = out.initialPreviewConfig || []; tags = out.initialPreviewThumbTags || []; append = out.append === undefined || out.append; if (content.length > 0 && !$h.isArray(content)) { content = content.split(self.initialPreviewDelimiter); } self._mergeArray('initialPreview', content); self._mergeArray('initialPreviewConfig', config); self._mergeArray('initialPreviewThumbTags', tags); if ($thumb !== undefined) { if (!allFiles) { index = self.previewCache.add(content, config[0], tags[0], append); data = self.previewCache.get(index, false); $div = $(document.createElement('div')).html(data).hide().insertAfter($thumb); $newCache = $div.find('.kv-zoom-cache'); if ($newCache && $newCache.length) { $newCache.insertAfter($thumb); } $thumb.fadeOut('slow', function () { var $newThumb = $div.find('.file-preview-frame'); if ($newThumb && $newThumb.length) { $newThumb.insertBefore($thumb).fadeIn('slow').css('display:inline-block'); } self._initPreviewActions(); self._clearFileInput(); $h.cleanZoomCache(self.$preview.find('#zoom-' + $thumb.attr('id'))); $thumb.remove(); $div.remove(); self._initSortable(); }); } else { i = $thumb.attr('data-fileindex'); self.uploadCache.content[i] = content[0]; self.uploadCache.config[i] = config[0] || []; self.uploadCache.tags[i] = tags[0] || []; self.uploadCache.append = append; } } else { self.previewCache.set(content, config, tags, append); self._initPreview(); self._initPreviewActions(); } } }, _initSuccessThumbs: function () { var self = this; if (!self.showPreview) { return; } self._getThumbs($h.FRAMES + '.file-preview-success').each(function () { var $thumb = $(this), $preview = self.$preview, $remove = $thumb.find('.kv-file-remove'); $remove.removeAttr('disabled'); self._handler($remove, 'click', function () { var id = $thumb.attr('id'), out = self._raise('filesuccessremove', [id, $thumb.attr('data-fileindex')]); $h.cleanMemory($thumb); if (out === false) { return; } $thumb.fadeOut('slow', function () { $h.cleanZoomCache($preview.find('#zoom-' + id)); $thumb.remove(); if (!self.getFrames().length) { self.reset(); } }); }); }); }, _checkAsyncComplete: function () { var self = this, previewId, i; for (i = 0; i < self.filestack.length; i++) { if (self.filestack[i]) { previewId = self.previewInitId + "-" + i; if ($.inArray(previewId, self.uploadLog) === -1) { return false; } } } return (self.uploadAsyncCount === self.uploadLog.length); }, _uploadExtra: function (previewId, index) { var self = this, data = self._getExtraData(previewId, index); if (data.length === 0) { return; } $.each(data, function (key, value) { self.formdata.append(key, value); }); }, _uploadSingle: function (i, isBatch) { var self = this, total = self.getFileStack().length, formdata = new FormData(), outData, previewId = self.previewInitId + "-" + i, $thumb, chkComplete, $btnUpload, $btnDelete, hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), uploadFailed, $prog = $('#' + previewId).find('.file-thumb-progress'), fnBefore, fnSuccess, fnComplete, fnError, updateUploadLog, params = {id: previewId, index: i}; self.formdata = formdata; if (self.showPreview) { $thumb = $('#' + previewId + ':not(.file-preview-initial)'); $btnUpload = $thumb.find('.kv-file-upload'); $btnDelete = $thumb.find('.kv-file-remove'); $prog.show(); } if (total === 0 || !hasPostData || ($btnUpload && $btnUpload.hasClass('disabled')) || self._abort(params)) { return; } updateUploadLog = function (i, previewId) { if (!uploadFailed) { self.updateStack(i, undefined); } self.uploadLog.push(previewId); if (self._checkAsyncComplete()) { self.fileBatchCompleted = true; } }; chkComplete = function () { var u = self.uploadCache, $initThumbs, i, j, len = 0, data = self.cacheInitialPreview; if (!self.fileBatchCompleted) { return; } if (data && data.content) { len = data.content.length; } setTimeout(function () { var triggerReset = self.getFileStack(true).length === 0; if (self.showPreview) { self.previewCache.set(u.content, u.config, u.tags, u.append); if (len) { for (i = 0; i < u.content.length; i++) { j = i + len; data.content[j] = u.content[i]; //noinspection JSUnresolvedVariable if (data.config.length) { data.config[j] = u.config[i]; } if (data.tags.length) { data.tags[j] = u.tags[i]; } } self.initialPreview = $h.cleanArray(data.content); self.initialPreviewConfig = $h.cleanArray(data.config); self.initialPreviewThumbTags = $h.cleanArray(data.tags); } else { self.initialPreview = u.content; self.initialPreviewConfig = u.config; self.initialPreviewThumbTags = u.tags; } self.cacheInitialPreview = {}; if (self.hasInitData) { self._initPreview(); self._initPreviewActions(); } } self.unlock(triggerReset); if (triggerReset) { self._clearFileInput(); } $initThumbs = self.$preview.find('.file-preview-initial'); if (self.uploadAsync && $initThumbs.length) { $h.addCss($initThumbs, $h.SORT_CSS); self._initSortable(); } self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]); self.uploadCount = 0; self.uploadStatus = {}; self.uploadLog = []; self._setProgress(101); self.ajaxAborted = false; }, 100); }; fnBefore = function (jqXHR) { outData = self._getOutData(jqXHR); self.fileBatchCompleted = false; if (!isBatch) { self.ajaxAborted = false; } if (self.showPreview) { if (!$thumb.hasClass('file-preview-success')) { self._setThumbStatus($thumb, 'Loading'); $h.addCss($thumb, 'file-uploading'); } $btnUpload.attr('disabled', true); $btnDelete.attr('disabled', true); } if (!isBatch) { self.lock(); } self._raise('filepreupload', [outData, previewId, i]); $.extend(true, params, outData); if (self._abort(params)) { jqXHR.abort(); if (!isBatch) { self._setThumbStatus($thumb, 'New'); $thumb.removeClass('file-uploading'); $btnUpload.removeAttr('disabled'); $btnDelete.removeAttr('disabled'); self.unlock(); } self._setProgressCancelled(); } }; fnSuccess = function (data, textStatus, jqXHR) { var pid = self.showPreview && $thumb.attr('id') ? $thumb.attr('id') : previewId; outData = self._getOutData(jqXHR, data); $.extend(true, params, outData); setTimeout(function () { if ($h.isEmpty(data) || $h.isEmpty(data.error)) { if (self.showPreview) { self._setThumbStatus($thumb, 'Success'); $btnUpload.hide(); self._initUploadSuccess(data, $thumb, isBatch); self._setProgress(101, $prog); } self._raise('fileuploaded', [outData, pid, i]); if (!isBatch) { self.updateStack(i, undefined); } else { updateUploadLog(i, pid); } } else { uploadFailed = true; self._showUploadError(data.error, params); self._setPreviewError($thumb, i, self.filestack[i], self.retryErrorUploads); if (!self.retryErrorUploads) { $btnUpload.hide(); } if (isBatch) { updateUploadLog(i, pid); } self._setProgress(101, $('#' + pid).find('.file-thumb-progress'), self.msgUploadError); } }, 100); }; fnComplete = function () { setTimeout(function () { if (self.showPreview) { $btnUpload.removeAttr('disabled'); $btnDelete.removeAttr('disabled'); $thumb.removeClass('file-uploading'); } if (!isBatch) { self.unlock(false); self._clearFileInput(); } else { chkComplete(); } self._initSuccessThumbs(); }, 100); }; fnError = function (jqXHR, textStatus, errorThrown) { var op = self.ajaxOperations.uploadThumb, errMsg = self._parseError(op, jqXHR, errorThrown, (isBatch && self.filestack[i].name ? self.filestack[i].name : null)); uploadFailed = true; setTimeout(function () { if (isBatch) { updateUploadLog(i, previewId); } self.uploadStatus[previewId] = 100; self._setPreviewError($thumb, i, self.filestack[i], self.retryErrorUploads); if (!self.retryErrorUploads) { $btnUpload.hide(); } $.extend(true, params, self._getOutData(jqXHR)); self._setProgress(101, $prog, self.msgAjaxProgressError.replace('{operation}', op)); self._setProgress(101, $('#' + previewId).find('.file-thumb-progress'), self.msgUploadError); self._showUploadError(errMsg, params); }, 100); }; formdata.append(self.uploadFileAttr, self.filestack[i], self.filenames[i]); formdata.append('file_id', i); self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError, previewId, i); }, _uploadBatch: function () { var self = this, files = self.filestack, total = files.length, params = {}, fnBefore, fnSuccess, fnError, fnComplete, hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), setAllUploaded; self.formdata = new FormData(); if (total === 0 || !hasPostData || self._abort(params)) { return; } setAllUploaded = function () { $.each(files, function (key) { self.updateStack(key, undefined); }); self._clearFileInput(); }; fnBefore = function (jqXHR) { self.lock(); var outData = self._getOutData(jqXHR); self.ajaxAborted = false; if (self.showPreview) { self._getThumbs().each(function () { var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'), $btnDelete = $thumb.find('.kv-file-remove'); if (!$thumb.hasClass('file-preview-success')) { self._setThumbStatus($thumb, 'Loading'); $h.addCss($thumb, 'file-uploading'); } $btnUpload.attr('disabled', true); $btnDelete.attr('disabled', true); }); } self._raise('filebatchpreupload', [outData]); if (self._abort(outData)) { jqXHR.abort(); self._getThumbs().each(function () { var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'), $btnDelete = $thumb.find('.kv-file-remove'); if ($thumb.hasClass('file-preview-loading')) { self._setThumbStatus($thumb, 'New'); $thumb.removeClass('file-uploading'); } $btnUpload.removeAttr('disabled'); $btnDelete.removeAttr('disabled'); }); self._setProgressCancelled(); } }; fnSuccess = function (data, textStatus, jqXHR) { /** @namespace data.errorkeys */ var outData = self._getOutData(jqXHR, data), key = 0, $thumbs = self._getThumbs(':not(.file-preview-success)'), keys = $h.isEmpty(data) || $h.isEmpty(data.errorkeys) ? [] : data.errorkeys; if ($h.isEmpty(data) || $h.isEmpty(data.error)) { self._raise('filebatchuploadsuccess', [outData]); setAllUploaded(); if (self.showPreview) { $thumbs.each(function () { var $thumb = $(this); self._setThumbStatus($thumb, 'Success'); $thumb.removeClass('file-uploading'); $thumb.find('.kv-file-upload').hide().removeAttr('disabled'); }); self._initUploadSuccess(data); } else { self.reset(); } self._setProgress(101); } else { if (self.showPreview) { $thumbs.each(function () { var $thumb = $(this), i = $thumb.attr('data-fileindex'); $thumb.removeClass('file-uploading'); $thumb.find('.kv-file-upload').removeAttr('disabled'); $thumb.find('.kv-file-remove').removeAttr('disabled'); if (keys.length === 0 || $.inArray(key, keys) !== -1) { self._setPreviewError($thumb, i, self.filestack[i], self.retryErrorUploads); if (!self.retryErrorUploads) { $thumb.find('.kv-file-upload').hide(); self.updateStack(i, undefined); } } else { $thumb.find('.kv-file-upload').hide(); self._setThumbStatus($thumb, 'Success'); self.updateStack(i, undefined); } if (!$thumb.hasClass('file-preview-error') || self.retryErrorUploads) { key++; } }); self._initUploadSuccess(data); } self._showUploadError(data.error, outData, 'filebatchuploaderror'); self._setProgress(101, self.$progress, self.msgUploadError); } }; fnComplete = function () { self.unlock(); self._initSuccessThumbs(); self._clearFileInput(); self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]); }; fnError = function (jqXHR, textStatus, errorThrown) { var outData = self._getOutData(jqXHR), op = self.ajaxOperations.uploadBatch, errMsg = self._parseError(op, jqXHR, errorThrown); self._showUploadError(errMsg, outData, 'filebatchuploaderror'); self.uploadFileCount = total - 1; if (!self.showPreview) { return; } self._getThumbs().each(function () { var $thumb = $(this), key = $thumb.attr('data-fileindex'); $thumb.removeClass('file-uploading'); if (self.filestack[key] !== undefined) { self._setPreviewError($thumb); } }); self._getThumbs().removeClass('file-uploading'); self._getThumbs(' .kv-file-upload').removeAttr('disabled'); self._getThumbs(' .kv-file-delete').removeAttr('disabled'); self._setProgress(101, self.$progress, self.msgAjaxProgressError.replace('{operation}', op)); }; $.each(files, function (key, data) { if (!$h.isEmpty(files[key])) { self.formdata.append(self.uploadFileAttr, data, self.filenames[key]); } }); self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); }, _uploadExtraOnly: function () { var self = this, params = {}, fnBefore, fnSuccess, fnComplete, fnError; self.formdata = new FormData(); if (self._abort(params)) { return; } fnBefore = function (jqXHR) { self.lock(); var outData = self._getOutData(jqXHR); self._raise('filebatchpreupload', [outData]); self._setProgress(50); params.data = outData; params.xhr = jqXHR; if (self._abort(params)) { jqXHR.abort(); self._setProgressCancelled(); } }; fnSuccess = function (data, textStatus, jqXHR) { var outData = self._getOutData(jqXHR, data); if ($h.isEmpty(data) || $h.isEmpty(data.error)) { self._raise('filebatchuploadsuccess', [outData]); self._clearFileInput(); self._initUploadSuccess(data); self._setProgress(101); } else { self._showUploadError(data.error, outData, 'filebatchuploaderror'); } }; fnComplete = function () { self.unlock(); self._clearFileInput(); self._raise('filebatchuploadcomplete', [self.filestack, self._getExtraData()]); }; fnError = function (jqXHR, textStatus, errorThrown) { var outData = self._getOutData(jqXHR), op = self.ajaxOperations.uploadExtra, errMsg = self._parseError(op, jqXHR, errorThrown); params.data = outData; self._showUploadError(errMsg, outData, 'filebatchuploaderror'); self._setProgress(101, self.$progress, self.msgAjaxProgressError.replace('{operation}', op)); }; self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); }, _deleteFileIndex: function ($frame) { var self = this, ind = $frame.attr('data-fileindex'); if (ind.substring(0, 5) === 'init_') { ind = parseInt(ind.replace('init_', '')); self.initialPreview = $h.spliceArray(self.initialPreview, ind); self.initialPreviewConfig = $h.spliceArray(self.initialPreviewConfig, ind); self.initialPreviewThumbTags = $h.spliceArray(self.initialPreviewThumbTags, ind); self.getFrames().each(function () { var $nFrame = $(this), nInd = $nFrame.attr('data-fileindex'); if (nInd.substring(0, 5) === 'init_') { nInd = parseInt(nInd.replace('init_', '')); if (nInd > ind) { nInd--; $nFrame.attr('data-fileindex', 'init_' + nInd); } } }); if (self.uploadAsync) { self.cacheInitialPreview = self.getPreview(); } } }, _initFileActions: function () { var self = this, $preview = self.$preview; if (!self.showPreview) { return; } self._initZoomButton(); self.getFrames(' .kv-file-remove').each(function () { var $el = $(this), $frame = $el.closest($h.FRAMES), hasError, id = $frame.attr('id'), ind = $frame.attr('data-fileindex'), n, cap, status; self._handler($el, 'click', function () { status = self._raise('filepreremove', [id, ind]); if (status === false || !self._validateMinCount()) { return false; } hasError = $frame.hasClass('file-preview-error'); $h.cleanMemory($frame); $frame.fadeOut('slow', function () { $h.cleanZoomCache($preview.find('#zoom-' + id)); self.updateStack(ind, undefined); self._clearObjects($frame); $frame.remove(); if (id && hasError) { self.$errorContainer.find('li[data-file-id="' + id + '"]').fadeOut('fast', function () { $(this).remove(); if (!self._errorsExist()) { self._resetErrors(); } }); } self._clearFileInput(); var filestack = self.getFileStack(true), chk = self.previewCache.count(), len = filestack.length, hasThumb = self.showPreview && self.getFrames().length; if (len === 0 && chk === 0 && !hasThumb) { self.reset(); } else { n = chk + len; cap = n > 1 ? self._getMsgSelected(n) : (filestack[0] ? self._getFileNames()[0] : ''); self._setCaption(cap); } self._raise('fileremoved', [id, ind]); }); }); }); self.getFrames(' .kv-file-upload').each(function () { var $el = $(this); self._handler($el, 'click', function () { var $frame = $el.closest($h.FRAMES), ind = $frame.attr('data-fileindex'); self.$progress.hide(); if ($frame.hasClass('file-preview-error') && !self.retryErrorUploads) { return; } self._uploadSingle(ind, false); }); }); }, _initPreviewActions: function () { var self = this, $preview = self.$preview, deleteExtraData = self.deleteExtraData || {}, btnRemove = $h.FRAMES + ' .kv-file-remove', settings = self.fileActionSettings, origClass = settings.removeClass, errClass = settings.removeErrorClass, resetProgress = function () { var hasFiles = self.isAjaxUpload ? self.previewCache.count() : self.$element.get(0).files.length; if (!$preview.find($h.FRAMES).length && !hasFiles) { self._setCaption(''); self.reset(); self.initialCaption = ''; } }; self._initZoomButton(); $preview.find(btnRemove).each(function () { var $el = $(this), vUrl = $el.data('url') || self.deleteUrl, vKey = $el.data('key'), fnBefore, fnSuccess, fnError; if ($h.isEmpty(vUrl) || vKey === undefined) { return; } var $frame = $el.closest($h.FRAMES), cache = self.previewCache.data, settings, params, index = $frame.attr('data-fileindex'), config, extraData; index = parseInt(index.replace('init_', '')); config = $h.isEmpty(cache.config) && $h.isEmpty(cache.config[index]) ? null : cache.config[index]; extraData = $h.isEmpty(config) || $h.isEmpty(config.extra) ? deleteExtraData : config.extra; if (typeof extraData === "function") { extraData = extraData(); } params = {id: $el.attr('id'), key: vKey, extra: extraData}; fnBefore = function (jqXHR) { self.ajaxAborted = false; self._raise('filepredelete', [vKey, jqXHR, extraData]); if (self._abort()) { jqXHR.abort(); } else { $el.removeClass(errClass); $h.addCss($frame, 'file-uploading'); $h.addCss($el, 'disabled ' + origClass); } }; fnSuccess = function (data, textStatus, jqXHR) { var n, cap; if (!$h.isEmpty(data) && !$h.isEmpty(data.error)) { params.jqXHR = jqXHR; params.response = data; self._showError(data.error, params, 'filedeleteerror'); $frame.removeClass('file-uploading'); $el.removeClass('disabled ' + origClass).addClass(errClass); resetProgress(); return; } $frame.removeClass('file-uploading').addClass('file-deleted'); $frame.fadeOut('slow', function () { index = parseInt(($frame.attr('data-fileindex')).replace('init_', '')); self.previewCache.unset(index); self._deleteFileIndex($frame); n = self.previewCache.count(); cap = n > 0 ? self._getMsgSelected(n) : ''; self._setCaption(cap); self._raise('filedeleted', [vKey, jqXHR, extraData]); $h.cleanZoomCache($preview.find('#zoom-' + $frame.attr('id'))); self._clearObjects($frame); $frame.remove(); resetProgress(); }); }; fnError = function (jqXHR, textStatus, errorThrown) { var op = self.ajaxOperations.deleteThumb, errMsg = self._parseError(op, jqXHR, errorThrown); params.jqXHR = jqXHR; params.response = {}; self._showError(errMsg, params, 'filedeleteerror'); $frame.removeClass('file-uploading'); $el.removeClass('disabled ' + origClass).addClass(errClass); resetProgress(); }; self._mergeAjaxCallback('beforeSend', fnBefore, 'delete'); self._mergeAjaxCallback('success', fnSuccess, 'delete'); self._mergeAjaxCallback('error', fnError, 'delete'); settings = $.extend(true, {}, { url: vUrl, type: 'POST', dataType: 'json', data: $.extend(true, {}, {key: vKey}, extraData) }, self.ajaxDeleteSettings); self._handler($el, 'click', function () { if (!self._validateMinCount()) { return false; } self.ajaxAborted = false; self._raise('filebeforedelete', [vKey, extraData]); //noinspection JSUnresolvedVariable,JSHint if (self.ajaxAborted instanceof Promise) { self.ajaxAborted.then(function (result) { if (!result) { $.ajax(settings); } }); } else { if (!self.ajaxAborted) { $.ajax(settings); } } }); }); }, _hideFileIcon: function () { var self = this; if (self.overwriteInitial) { self.$captionContainer.removeClass('icon-visible'); } }, _showFileIcon: function () { var self = this; $h.addCss(self.$captionContainer, 'icon-visible'); }, _getSize: function (bytes) { var self = this, size = parseFloat(bytes), i, func = self.fileSizeGetter, sizes, out; if (!$.isNumeric(bytes) || !$.isNumeric(size)) { return ''; } if (typeof func === 'function') { out = func(size); } else { if (size === 0) { out = '0.00 B'; } else { i = Math.floor(Math.log(size) / Math.log(1024)); sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; out = (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; } } return self._getLayoutTemplate('size').replace('{sizeText}', out); }, _generatePreviewTemplate: function (cat, data, fname, ftype, previewId, isError, size, frameClass, foot, ind, templ) { var self = this, caption = self.slug(fname), prevContent, zoomContent = '', styleAttribs = '', screenW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, config = screenW < 400 ? (self.previewSettingsSmall[cat] || self.defaults.previewSettingsSmall[cat]) : (self.previewSettings[cat] || self.defaults.previewSettings[cat]), footer = foot || self._renderFileFooter(caption, size, 'auto', isError), hasIconSetting = self._getPreviewIcon(fname), typeCss = 'type-default', forcePrevIcon = hasIconSetting && self.preferIconicPreview, forceZoomIcon = hasIconSetting && self.preferIconicZoomPreview, getContent; if (config) { $.each(config, function (key, val) { styleAttribs += key + ':' + val + ';'; }); } getContent = function (c, d, zoom, frameCss) { var id = zoom ? 'zoom-' + previewId : previewId, tmplt = self._getPreviewTemplate(c), css = (frameClass || '') + ' ' + frameCss; if (self.frameClass) { css = self.frameClass + ' ' + css; } if (zoom) { css = css.replace(' ' + $h.SORT_CSS, ''); } tmplt = self._parseFilePreviewIcon(tmplt, fname); if (c === 'text') { d = $h.htmlEncode(d); } if (cat === 'object' && !ftype) { $.each(self.defaults.fileTypeSettings, function (key, func) { if (key === 'object' || key === 'other') { return; } if (func(fname, ftype)) { typeCss = 'type-' + key; } }); } return tmplt.setTokens({ 'previewId': id, 'caption': caption, 'frameClass': css, 'type': ftype, 'fileindex': ind, 'typeCss': typeCss, 'footer': footer, 'data': d, 'template': templ || cat, 'style': styleAttribs ? 'style="' + styleAttribs + '"' : '' }); }; ind = ind || previewId.slice(previewId.lastIndexOf('-') + 1); if (self.fileActionSettings.showZoom) { zoomContent = getContent((forceZoomIcon ? 'other' : cat), data, true, 'kv-zoom-thumb'); } zoomContent = '\n' + self._getLayoutTemplate('zoomCache').replace('{zoomContent}', zoomContent); prevContent = getContent((forcePrevIcon ? 'other' : cat), data, false, 'kv-preview-thumb'); return prevContent + zoomContent; }, _previewDefault: function (file, previewId, isDisabled) { var self = this, $preview = self.$preview; if (!self.showPreview) { return; } var fname = file ? file.name : '', ftype = file ? file.type : '', content, size = file.size || 0, caption = self.slug(fname), isError = isDisabled === true && !self.isAjaxUpload, data = $h.objUrl.createObjectURL(file); self._clearDefaultPreview(); content = self._generatePreviewTemplate('other', data, fname, ftype, previewId, isError, size); $preview.append("\n" + content); self._setThumbAttr(previewId, caption, size); if (isDisabled === true && self.isAjaxUpload) { self._setThumbStatus($('#' + previewId), 'Error'); } }, _previewFile: function (i, file, theFile, previewId, data, fileInfo) { if (!this.showPreview) { return; } var self = this, fname = file ? file.name : '', ftype = fileInfo.type, caption = fileInfo.name, cat = self._parseFileType(ftype, fname), types = self.allowedPreviewTypes, content, mimes = self.allowedPreviewMimeTypes, $preview = self.$preview, fsize = file.size || 0, chkTypes = types && types.indexOf(cat) >= 0, chkMimes = mimes && mimes.indexOf(ftype) !== -1, iData = (cat === 'text' || cat === 'html' || cat === 'image') ? theFile.target.result : data; /** @namespace window.DOMPurify */ if (cat === 'html' && self.purifyHtml && window.DOMPurify) { iData = window.DOMPurify.sanitize(iData); } if (chkTypes || chkMimes) { content = self._generatePreviewTemplate(cat, iData, fname, ftype, previewId, false, fsize); self._clearDefaultPreview(); $preview.append("\n" + content); var $img = $preview.find('#' + previewId + ' img'); if ($img.length && self.autoOrientImage) { $h.validateOrientation(file, function (value) { if (!value) { self._validateImage(previewId, caption, ftype, fsize, iData); return; } var $zoomImg = $preview.find('#zoom-' + previewId + ' img'), css = 'rotate-' + value; if (value > 4) { css += ($img.width() > $img.height() ? ' is-portrait-gt4' : ' is-landscape-gt4'); } $h.addCss($img, css); $h.addCss($zoomImg, css); self._raise('fileimageoriented', {'$img': $img, 'file': file}); self._validateImage(previewId, caption, ftype, fsize, iData); $h.adjustOrientedImage($img); }); } else { self._validateImage(previewId, caption, ftype, fsize, iData); } } else { self._previewDefault(file, previewId); } self._setThumbAttr(previewId, caption, fsize); self._initSortable(); }, _setThumbAttr: function (id, caption, size) { var self = this, $frame = $('#' + id); if ($frame.length) { size = size && size > 0 ? self._getSize(size) : ''; $frame.data({'caption': caption, 'size': size}); } }, _setInitThumbAttr: function () { var self = this, data = self.previewCache.data, len = self.previewCache.count(), config, caption, size, previewId; if (len === 0) { return; } for (var i = 0; i < len; i++) { config = data.config[i]; previewId = self.previewInitId + '-' + 'init_' + i; caption = $h.ifSet('caption', config, $h.ifSet('filename', config)); size = $h.ifSet('size', config); self._setThumbAttr(previewId, caption, size); } }, _slugDefault: function (text) { return $h.isEmpty(text) ? '' : String(text).replace(/[\[\]\/\{}:;#%=\(\)\*\+\?\\\^\$\|<>&"']/g, '_'); }, _updateFileDetails: function (numFiles) { var self = this, $el = self.$element, fileStack = self.getFileStack(), name = ($h.isIE(9) && $h.findFileName($el.val())) || ($el[0].files[0] && $el[0].files[0].name) || (fileStack.length && fileStack[0].name) || '', label = self.slug(name), n = self.isAjaxUpload ? fileStack.length : numFiles, nFiles = self.previewCache.count() + n, log = n === 1 ? label : self._getMsgSelected(nFiles); if (self.isError) { self.$previewContainer.removeClass('file-thumb-loading'); self.$previewStatus.html(''); self.$captionContainer.removeClass('icon-visible'); } else { self._showFileIcon(); } self._setCaption(log, self.isError); self.$container.removeClass('file-input-new file-input-ajax-new'); if (arguments.length === 1) { self._raise('fileselect', [numFiles, label]); } if (self.previewCache.count()) { self._initPreviewActions(); } }, _setThumbStatus: function ($thumb, status) { var self = this; if (!self.showPreview) { return; } var icon = 'indicator' + status, msg = icon + 'Title', css = 'file-preview-' + status.toLowerCase(), $indicator = $thumb.find('.file-upload-indicator'), config = self.fileActionSettings; $thumb.removeClass('file-preview-success file-preview-error file-preview-loading'); if (status === 'Success') { $thumb.find('.file-drag-handle').remove(); } $indicator.html(config[icon]); $indicator.attr('title', config[msg]); $thumb.addClass(css); if (status === 'Error' && !self.retryErrorUploads) { $thumb.find('.kv-file-upload').attr('disabled', true); } }, _setProgressCancelled: function () { var self = this; self._setProgress(101, self.$progress, self.msgCancelled); }, _setProgress: function (p, $el, error) { var self = this, pct = Math.min(p, 100), out, pctLimit = self.progressUploadThreshold, t = p <= 100 ? self.progressTemplate : self.progressCompleteTemplate, template = pct < 100 ? self.progressTemplate : (error ? self.progressErrorTemplate : t); $el = $el || self.$progress; if (!$h.isEmpty(template)) { if (pctLimit && pct > pctLimit && p <= 100) { out = template.setTokens({'percent': pctLimit, 'status': self.msgUploadThreshold}); } else { out = template.setTokens({'percent': pct, 'status': (p > 100 ? self.msgUploadEnd : pct + '%')}); } $el.html(out); if (error) { $el.find('[role="progressbar"]').html(error); } } }, _setFileDropZoneTitle: function () { var self = this, $zone = self.$container.find('.file-drop-zone'), title = self.dropZoneTitle, strFiles; if (self.isClickable) { strFiles = $h.isEmpty(self.$element.attr('multiple')) ? self.fileSingle : self.filePlural; title += self.dropZoneClickTitle.replace('{files}', strFiles); } $zone.find('.' + self.dropZoneTitleClass).remove(); if (!self.isAjaxUpload || !self.showPreview || $zone.length === 0 || self.getFileStack().length > 0 || !self.dropZoneEnabled) { return; } if ($zone.find($h.FRAMES).length === 0 && $h.isEmpty(self.defaultPreviewContent)) { $zone.prepend('
    ' + title + '
    '); } self.$container.removeClass('file-input-new'); $h.addCss(self.$container, 'file-input-ajax-new'); }, _setAsyncUploadStatus: function (previewId, pct, total) { var self = this, sum = 0; self._setProgress(pct, $('#' + previewId).find('.file-thumb-progress')); self.uploadStatus[previewId] = pct; $.each(self.uploadStatus, function (key, value) { sum += value; }); self._setProgress(Math.floor(sum / total)); }, _validateMinCount: function () { var self = this, len = self.isAjaxUpload ? self.getFileStack().length : self.$element.get(0).files.length; if (self.validateInitialCount && self.minFileCount > 0 && self._getFileCount(len - 1) < self.minFileCount) { self._noFilesError({}); return false; } return true; }, _getFileCount: function (fileCount) { var self = this, addCount = 0; if (self.validateInitialCount && !self.overwriteInitial) { addCount = self.previewCache.count(); fileCount += addCount; } return fileCount; }, _getFileId: function (file) { var self = this, custom = self.generateFileId, relativePath; if (typeof custom === 'function') { return custom(file, event); } if (!file) { return null; } /** @namespace file.webkitRelativePath */ relativePath = String(file.webkitRelativePath || file.fileName || file.name || null); if (!relativePath) { return null; } return (file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')); }, _getFileName: function (file) { return file && file.name ? this.slug(file.name) : undefined; }, _getFileIds: function (skipNull) { var self = this; return self.fileids.filter(function (n) { return (skipNull ? n !== undefined : n !== undefined && n !== null); }); }, _getFileNames: function (skipNull) { var self = this; return self.filenames.filter(function (n) { return (skipNull ? n !== undefined : n !== undefined && n !== null); }); }, _setPreviewError: function ($thumb, i, val, repeat) { var self = this; if (i !== undefined) { self.updateStack(i, val); } if (!self.showPreview) { return; } if (self.removeFromPreviewOnError && !repeat) { $thumb.remove(); return; } else { self._setThumbStatus($thumb, 'Error'); } self._refreshUploadButton($thumb, repeat); }, _refreshUploadButton: function ($thumb, repeat) { var self = this, $btn = $thumb.find('.kv-file-upload'), cfg = self.fileActionSettings, icon = cfg.uploadIcon, title = cfg.uploadTitle; if (!$btn.length) { return; } if (repeat) { icon = cfg.uploadRetryIcon; title = cfg.uploadRetryTitle; } $btn.attr('title', title).html(icon); }, _checkDimensions: function (i, chk, $img, $thumb, fname, type, params) { var self = this, msg, dim, tag = chk === 'Small' ? 'min' : 'max', limit = self[tag + 'Image' + type], $imgEl, isValid; if ($h.isEmpty(limit) || !$img.length) { return; } $imgEl = $img[0]; dim = (type === 'Width') ? $imgEl.naturalWidth || $imgEl.width : $imgEl.naturalHeight || $imgEl.height; isValid = chk === 'Small' ? dim >= limit : dim <= limit; if (isValid) { return; } msg = self['msgImage' + type + chk].setTokens({'name': fname, 'size': limit}); self._showUploadError(msg, params); self._setPreviewError($thumb, i, null); }, _validateImage: function (previewId, fname, ftype, fsize, iData) { var self = this, $preview = self.$preview, params, w1, w2, $thumb = $preview.find("#" + previewId), i = $thumb.attr('data-fileindex'), $img = $thumb.find('img'), exifObject; fname = fname || 'Untitled'; $img.one('load', function () { w1 = $thumb.width(); w2 = $preview.width(); if (w1 > w2) { $img.css('width', '100%'); } params = {ind: i, id: previewId}; self._checkDimensions(i, 'Small', $img, $thumb, fname, 'Width', params); self._checkDimensions(i, 'Small', $img, $thumb, fname, 'Height', params); if (!self.resizeImage) { self._checkDimensions(i, 'Large', $img, $thumb, fname, 'Width', params); self._checkDimensions(i, 'Large', $img, $thumb, fname, 'Height', params); } self._raise('fileimageloaded', [previewId]); try { exifObject = window.piexif ? window.piexif.load(iData) : null; } catch (err) { exifObject = null; } self.loadedImages.push({ ind: i, img: $img, thumb: $thumb, pid: previewId, typ: ftype, siz: fsize, validated: false, imgData: iData, exifObj: exifObject }); $thumb.data('exif', exifObject); self._validateAllImages(); }).one('error', function () { self._raise('fileimageloaderror', [previewId]); }).each(function () { if (this.complete) { $(this).trigger('load'); } else { if (this.error) { $(this).trigger('error'); } } }); }, _validateAllImages: function () { var self = this, i, counter = {val: 0}, numImgs = self.loadedImages.length, config, fsize, minSize = self.resizeIfSizeMoreThan; if (numImgs !== self.totalImagesCount) { return; } self._raise('fileimagesloaded'); if (!self.resizeImage) { return; } for (i = 0; i < self.loadedImages.length; i++) { config = self.loadedImages[i]; if (config.validated) { continue; } fsize = config.siz; if (fsize && fsize > minSize * 1000) { self._getResizedImage(config, counter, numImgs); } self.loadedImages[i].validated = true; } }, _getResizedImage: function (config, counter, numImgs) { var self = this, img = $(config.img)[0], width = img.naturalWidth, height = img.naturalHeight, blob, ratio = 1, maxWidth = self.maxImageWidth || width, maxHeight = self.maxImageHeight || height, isValidImage = !!(width && height), chkWidth, chkHeight, canvas = self.imageCanvas, dataURI, context = self.imageCanvasContext, type = config.typ, pid = config.pid, ind = config.ind, $thumb = config.thumb, throwError, msg, exifObj = config.exifObj, exifStr; throwError = function (msg, params, ev) { if (self.isAjaxUpload) { self._showUploadError(msg, params, ev); } else { self._showError(msg, params, ev); } self._setPreviewError($thumb, ind); }; if (!self.filestack[ind] || !isValidImage || (width <= maxWidth && height <= maxHeight)) { if (isValidImage && self.filestack[ind]) { self._raise('fileimageresized', [pid, ind]); } counter.val++; if (counter.val === numImgs) { self._raise('fileimagesresized'); } if (!isValidImage) { throwError(self.msgImageResizeError, {id: pid, 'index': ind}, 'fileimageresizeerror'); return; } } type = type || self.resizeDefaultImageType; chkWidth = width > maxWidth; chkHeight = height > maxHeight; if (self.resizePreference === 'width') { ratio = chkWidth ? maxWidth / width : (chkHeight ? maxHeight / height : 1); } else { ratio = chkHeight ? maxHeight / height : (chkWidth ? maxWidth / width : 1); } self._resetCanvas(); width *= ratio; height *= ratio; canvas.width = width; canvas.height = height; try { context.drawImage(img, 0, 0, width, height); dataURI = canvas.toDataURL(type, self.resizeQuality); if (exifObj) { exifStr = window.piexif.dump(exifObj); dataURI = window.piexif.insert(exifStr, dataURI); } blob = $h.dataURI2Blob(dataURI); self.filestack[ind] = blob; self._raise('fileimageresized', [pid, ind]); counter.val++; if (counter.val === numImgs) { self._raise('fileimagesresized', [undefined, undefined]); } if (!(blob instanceof Blob)) { throwError(self.msgImageResizeError, {id: pid, 'index': ind}, 'fileimageresizeerror'); } } catch (err) { counter.val++; if (counter.val === numImgs) { self._raise('fileimagesresized', [undefined, undefined]); } msg = self.msgImageResizeException.replace('{errors}', err.message); throwError(msg, {id: pid, 'index': ind}, 'fileimageresizeexception'); } }, _initBrowse: function ($container) { var self = this; if (self.showBrowse) { self.$btnFile = $container.find('.btn-file'); self.$btnFile.append(self.$element); } else { self.$element.hide(); } }, _initCaption: function () { var self = this, cap = self.initialCaption || ''; if (self.overwriteInitial || $h.isEmpty(cap)) { self.$caption.val(''); return false; } self._setCaption(cap); return true; }, _setCaption: function (content, isError) { var self = this, title, out, icon, n, cap, stack = self.getFileStack(); if (!self.$caption.length) { return; } self.$captionContainer.removeClass('icon-visible'); if (isError) { title = $('
    ' + self.msgValidationError + '
    ').text(); n = stack.length; if (n) { cap = n === 1 && stack[0] ? self._getFileNames()[0] : self._getMsgSelected(n); } else { cap = self._getMsgSelected(self.msgNo); } out = $h.isEmpty(content) ? cap : content; icon = '' + self.msgValidationErrorIcon + ''; } else { if ($h.isEmpty(content)) { return; } title = $('
    ' + content + '
    ').text(); out = title; icon = self._getLayoutTemplate('fileIcon'); } self.$captionContainer.addClass('icon-visible'); self.$caption.attr('title', title).val(out); self.$captionIcon.html(icon); }, _createContainer: function () { var self = this, attribs = {"class": 'file-input file-input-new' + (self.rtl ? ' kv-rtl' : '')}, $container = $(document.createElement("div")).attr(attribs).html(self._renderMain()); self.$element.before($container); self._initBrowse($container); if (self.theme) { $container.addClass('theme-' + self.theme); } return $container; }, _refreshContainer: function () { var self = this, $container = self.$container; $container.before(self.$element); $container.html(self._renderMain()); self._initBrowse($container); self._validateDisabled(); }, _validateDisabled: function () { var self = this; self.$caption.attr({readonly: self.isDisabled}); }, _renderMain: function () { var self = this, dropCss = (self.isAjaxUpload && self.dropZoneEnabled) ? ' file-drop-zone' : 'file-drop-disabled', close = !self.showClose ? '' : self._getLayoutTemplate('close'), preview = !self.showPreview ? '' : self._getLayoutTemplate('preview') .setTokens({'class': self.previewClass, 'dropClass': dropCss}), css = self.isDisabled ? self.captionClass + ' file-caption-disabled' : self.captionClass, caption = self.captionTemplate.setTokens({'class': css + ' kv-fileinput-caption'}); return self.mainTemplate.setTokens({ 'class': self.mainClass + (!self.showBrowse && self.showCaption ? ' no-browse' : ''), 'preview': preview, 'close': close, 'caption': caption, 'upload': self._renderButton('upload'), 'remove': self._renderButton('remove'), 'cancel': self._renderButton('cancel'), 'browse': self._renderButton('browse') }); }, _renderButton: function (type) { var self = this, tmplt = self._getLayoutTemplate('btnDefault'), css = self[type + 'Class'], title = self[type + 'Title'], icon = self[type + 'Icon'], label = self[type + 'Label'], status = self.isDisabled ? ' disabled' : '', btnType = 'button'; switch (type) { case 'remove': if (!self.showRemove) { return ''; } break; case 'cancel': if (!self.showCancel) { return ''; } css += ' kv-hidden'; break; case 'upload': if (!self.showUpload) { return ''; } if (self.isAjaxUpload && !self.isDisabled) { tmplt = self._getLayoutTemplate('btnLink').replace('{href}', self.uploadUrl); } else { btnType = 'submit'; } break; case 'browse': if (!self.showBrowse) { return ''; } tmplt = self._getLayoutTemplate('btnBrowse'); break; default: return ''; } css += type === 'browse' ? ' btn-file' : ' fileinput-' + type + ' fileinput-' + type + '-button'; if (!$h.isEmpty(label)) { label = ' ' + label + ''; } return tmplt.setTokens({ 'type': btnType, 'css': css, 'title': title, 'status': status, 'icon': icon, 'label': label }); }, _renderThumbProgress: function () { var self = this; return '
    ' + self.progressTemplate.setTokens({'percent': '0', 'status': self.msgUploadBegin}) + '
    '; }, _renderFileFooter: function (caption, size, width, isError) { var self = this, config = self.fileActionSettings, rem = config.showRemove, drg = config.showDrag, upl = config.showUpload, zoom = config.showZoom, out, template = self._getLayoutTemplate('footer'), tInd = self._getLayoutTemplate('indicator'), ind = isError ? config.indicatorError : config.indicatorNew, title = isError ? config.indicatorErrorTitle : config.indicatorNewTitle, indicator = tInd.setTokens({'indicator': ind, 'indicatorTitle': title}); size = self._getSize(size); if (self.isAjaxUpload) { out = template.setTokens({ 'actions': self._renderFileActions(upl, false, rem, zoom, drg, false, false, false), 'caption': caption, 'size': size, 'width': width, 'progress': self._renderThumbProgress(), 'indicator': indicator }); } else { out = template.setTokens({ 'actions': self._renderFileActions(false, false, false, zoom, drg, false, false, false), 'caption': caption, 'size': size, 'width': width, 'progress': '', 'indicator': indicator }); } out = $h.replaceTags(out, self.previewThumbTags); return out; }, _renderFileActions: function (showUpl, showDwn, showDel, showZoom, showDrag, disabled, url, key, isInit, dUrl, dFile) { if (!showUpl && !showDwn && !showDel && !showZoom && !showDrag) { return ''; } var self = this, vUrl = url === false ? '' : ' data-url="' + url + '"', vKey = key === false ? '' : ' data-key="' + key + '"', btnDelete = '', btnUpload = '', btnDownload = '', btnZoom = '', btnDrag = '', css, template = self._getLayoutTemplate('actions'), config = self.fileActionSettings, otherButtons = self.otherActionButtons.setTokens({'dataKey': vKey, 'key': key}), removeClass = disabled ? config.removeClass + ' disabled' : config.removeClass; if (showDel) { btnDelete = self._getLayoutTemplate('actionDelete').setTokens({ 'removeClass': removeClass, 'removeIcon': config.removeIcon, 'removeTitle': config.removeTitle, 'dataUrl': vUrl, 'dataKey': vKey, 'key': key }); } if (showUpl) { btnUpload = self._getLayoutTemplate('actionUpload').setTokens({ 'uploadClass': config.uploadClass, 'uploadIcon': config.uploadIcon, 'uploadTitle': config.uploadTitle }); } if (showDwn) { btnDownload = self._getLayoutTemplate('actionDownload').setTokens({ 'downloadClass': config.downloadClass, 'downloadIcon': config.downloadIcon, 'downloadTitle': config.downloadTitle, 'downloadUrl': dUrl || self.initialPreviewDownloadUrl }); btnDownload = btnDownload.setTokens({'filename': dFile, 'key': key}); } if (showZoom) { btnZoom = self._getLayoutTemplate('actionZoom').setTokens({ 'zoomClass': config.zoomClass, 'zoomIcon': config.zoomIcon, 'zoomTitle': config.zoomTitle }); } if (showDrag && isInit) { css = 'drag-handle-init ' + config.dragClass; btnDrag = self._getLayoutTemplate('actionDrag').setTokens({ 'dragClass': css, 'dragTitle': config.dragTitle, 'dragIcon': config.dragIcon }); } return template.setTokens({ 'delete': btnDelete, 'upload': btnUpload, 'download': btnDownload, 'zoom': btnZoom, 'drag': btnDrag, 'other': otherButtons }); }, _browse: function (e) { var self = this; self._raise('filebrowse'); if (e && e.isDefaultPrevented()) { return; } if (self.isError && !self.isAjaxUpload) { self.clear(); } self.$captionContainer.focus(); }, _filterDuplicate: function (file, files, fileIds) { var self = this, fileId = self._getFileId(file); if (fileId && fileIds && fileIds.indexOf(fileId) > -1) { return; } if (!fileIds) { fileIds = []; } files.push(file); fileIds.push(fileId); }, _change: function (e) { var self = this, $el = self.$element; if (!self.isAjaxUpload && $h.isEmpty($el.val()) && self.fileInputCleared) { // IE 11 fix self.fileInputCleared = false; return; } self.fileInputCleared = false; var tfiles = [], msg, total, isDragDrop = arguments.length > 1, isAjaxUpload = self.isAjaxUpload, n, len, files = isDragDrop ? e.originalEvent.dataTransfer.files : $el.get(0).files, ctr = self.filestack.length, isSingleUpload = $h.isEmpty($el.attr('multiple')), flagSingle = (isSingleUpload && ctr > 0), folders = 0, fileIds = self._getFileIds(), throwError = function (mesg, file, previewId, index) { var p1 = $.extend(true, {}, self._getOutData({}, {}, files), {id: previewId, index: index}), p2 = {id: previewId, index: index, file: file, files: files}; return self.isAjaxUpload ? self._showUploadError(mesg, p1) : self._showError(mesg, p2); }; self.reader = null; self._resetUpload(); self._hideFileIcon(); if (self.isAjaxUpload) { self.$container.find('.file-drop-zone .' + self.dropZoneTitleClass).remove(); } if (isDragDrop) { $.each(files, function (i, f) { if (f && !f.type && f.size !== undefined && f.size % 4096 === 0) { folders++; } else { self._filterDuplicate(f, tfiles, fileIds); } }); } else { if (e.target && e.target.files === undefined) { files = e.target.value ? [{name: e.target.value.replace(/^.+\\/, '')}] : []; } else { files = e.target.files || {}; } if (isAjaxUpload) { $.each(files, function (i, f) { self._filterDuplicate(f, tfiles, fileIds); }); } else { tfiles = files; } } if ($h.isEmpty(tfiles) || tfiles.length === 0) { if (!isAjaxUpload) { self.clear(); } self._showFolderError(folders); self._raise('fileselectnone'); return; } self._resetErrors(); len = tfiles.length; total = self._getFileCount(self.isAjaxUpload ? (self.getFileStack().length + len) : len); if (self.maxFileCount > 0 && total > self.maxFileCount) { if (!self.autoReplace || len > self.maxFileCount) { n = (self.autoReplace && len > self.maxFileCount) ? len : total; msg = self.msgFilesTooMany.replace('{m}', self.maxFileCount).replace('{n}', n); self.isError = throwError(msg, null, null, null); self.$captionContainer.removeClass('icon-visible'); self._setCaption('', true); self.$container.removeClass('file-input-new file-input-ajax-new'); return; } if (total > self.maxFileCount) { self._resetPreviewThumbs(isAjaxUpload); } } else { if (!isAjaxUpload || flagSingle) { self._resetPreviewThumbs(false); if (flagSingle) { self.clearStack(); } } else { if (isAjaxUpload && ctr === 0 && (!self.previewCache.count() || self.overwriteInitial)) { self._resetPreviewThumbs(true); } } } if (self.isPreviewable) { self.readFiles(tfiles); } else { self._updateFileDetails(1); } self._showFolderError(folders); }, _abort: function (params) { var self = this, data; if (self.ajaxAborted && typeof self.ajaxAborted === "object" && self.ajaxAborted.message !== undefined) { data = $.extend(true, {}, self._getOutData(), params); data.abortData = self.ajaxAborted.data || {}; data.abortMessage = self.ajaxAborted.message; self._setProgress(101, self.$progress, self.msgCancelled); self._showUploadError(self.ajaxAborted.message, data, 'filecustomerror'); self.cancel(); return true; } return !!self.ajaxAborted; }, _resetFileStack: function () { var self = this, i = 0, newstack = [], newnames = [], newids = []; self._getThumbs().each(function () { var $thumb = $(this), ind = $thumb.attr('data-fileindex'), file = self.filestack[ind], pid = $thumb.attr('id'), newId; if (ind === '-1' || ind === -1) { return; } if (file !== undefined) { newstack[i] = file; newnames[i] = self._getFileName(file); newids[i] = self._getFileId(file); $thumb.attr({'id': self.previewInitId + '-' + i, 'data-fileindex': i}); i++; } else { newId = 'uploaded-' + $h.uniqId(); $thumb.attr({'id': newId, 'data-fileindex': '-1'}); self.$preview.find('#zoom-' + pid).attr('id', 'zoom-' + newId); } }); self.filestack = newstack; self.filenames = newnames; self.fileids = newids; }, _isFileSelectionValid: function (cnt) { var self = this; cnt = cnt || 0; if (self.required && !self.getFilesCount()) { self.$errorContainer.html(''); self._showUploadError(self.msgFileRequired); return false; } if (self.minFileCount > 0 && self._getFileCount(cnt) < self.minFileCount) { self._noFilesError({}); return false; } return true; }, clearStack: function () { var self = this; self.filestack = []; self.filenames = []; self.fileids = []; return self.$element; }, updateStack: function (i, file) { var self = this; self.filestack[i] = file; self.filenames[i] = self._getFileName(file); self.fileids[i] = file && self._getFileId(file) || null; return self.$element; }, addToStack: function (file) { var self = this; self.filestack.push(file); self.filenames.push(self._getFileName(file)); self.fileids.push(self._getFileId(file)); return self.$element; }, getFileStack: function (skipNull) { var self = this; return self.filestack.filter(function (n) { return (skipNull ? n !== undefined : n !== undefined && n !== null); }); }, getFilesCount: function () { var self = this, len = self.isAjaxUpload ? self.getFileStack().length : self.$element.get(0).files.length; return self._getFileCount(len); }, readFiles: function (files) { this.reader = new FileReader(); var self = this, $el = self.$element, $preview = self.$preview, reader = self.reader, $container = self.$previewContainer, $status = self.$previewStatus, msgLoading = self.msgLoading, msgProgress = self.msgProgress, previewInitId = self.previewInitId, numFiles = files.length, settings = self.fileTypeSettings, ctr = self.filestack.length, readFile, fileTypes = self.allowedFileTypes, typLen = fileTypes ? fileTypes.length : 0, fileExt = self.allowedFileExtensions, strExt = $h.isEmpty(fileExt) ? '' : fileExt.join(', '), maxPreviewSize = self.maxFilePreviewSize && parseFloat(self.maxFilePreviewSize), canPreview = $preview.length && (!maxPreviewSize || isNaN(maxPreviewSize)), throwError = function (msg, file, previewId, index) { var p1 = $.extend(true, {}, self._getOutData({}, {}, files), {id: previewId, index: index}), p2 = {id: previewId, index: index, file: file, files: files}, $thumb; self._previewDefault(file, previewId, true); if (self.isAjaxUpload) { self.addToStack(undefined); setTimeout(function () { readFile(index + 1); }, 100); } else { numFiles = 0; } self._initFileActions(); $thumb = $('#' + previewId); $thumb.find('.kv-file-upload').hide(); if (self.removeFromPreviewOnError) { $thumb.remove(); } self.isError = self.isAjaxUpload ? self._showUploadError(msg, p1) : self._showError(msg, p2); self._updateFileDetails(numFiles); }; self.loadedImages = []; self.totalImagesCount = 0; $.each(files, function (key, file) { var func = self.fileTypeSettings.image; if (func && func(file.type)) { self.totalImagesCount++; } }); readFile = function (i) { if ($h.isEmpty($el.attr('multiple'))) { numFiles = 1; } if (i >= numFiles) { if (self.isAjaxUpload && self.filestack.length > 0) { self._raise('filebatchselected', [self.getFileStack()]); } else { self._raise('filebatchselected', [files]); } $container.removeClass('file-thumb-loading'); $status.html(''); return; } var node = ctr + i, previewId = previewInitId + "-" + node, file = files[i], fSizeKB, j, msg, fnText = settings.text, fnImage = settings.image, fnHtml = settings.html, typ, chk, typ1, typ2, caption = file.name ? self.slug(file.name) : '', fileSize = (file.size || 0) / 1000, fileExtExpr = '', previewData = $h.objUrl.createObjectURL(file), fileCount = 0, strTypes = '', func, knownTypes = 0, isText, isHtml, isImage, txtFlag, processFileLoaded = function () { var msg = msgProgress.setTokens({ 'index': i + 1, 'files': numFiles, 'percent': 50, 'name': caption }); setTimeout(function () { $status.html(msg); self._updateFileDetails(numFiles); readFile(i + 1); }, 100); self._raise('fileloaded', [file, previewId, i, reader]); }; if (typLen > 0) { for (j = 0; j < typLen; j++) { typ1 = fileTypes[j]; typ2 = self.msgFileTypes[typ1] || typ1; strTypes += j === 0 ? typ2 : ', ' + typ2; } } if (caption === false) { readFile(i + 1); return; } if (caption.length === 0) { msg = self.msgInvalidFileName.replace('{name}', $h.htmlEncode(file.name)); throwError(msg, file, previewId, i); return; } if (!$h.isEmpty(fileExt)) { fileExtExpr = new RegExp('\\.(' + fileExt.join('|') + ')$', 'i'); } fSizeKB = fileSize.toFixed(2); if (self.maxFileSize > 0 && fileSize > self.maxFileSize) { msg = self.msgSizeTooLarge.setTokens({ 'name': caption, 'size': fSizeKB, 'maxSize': self.maxFileSize }); throwError(msg, file, previewId, i); return; } if (self.minFileSize !== null && fileSize <= $h.getNum(self.minFileSize)) { msg = self.msgSizeTooSmall.setTokens({ 'name': caption, 'size': fSizeKB, 'minSize': self.minFileSize }); throwError(msg, file, previewId, i); return; } if (!$h.isEmpty(fileTypes) && $h.isArray(fileTypes)) { for (j = 0; j < fileTypes.length; j += 1) { typ = fileTypes[j]; func = settings[typ]; fileCount += !func || (typeof func !== 'function') ? 0 : (func(file.type, file.name) ? 1 : 0); } if (fileCount === 0) { msg = self.msgInvalidFileType.setTokens({'name': caption, 'types': strTypes}); throwError(msg, file, previewId, i); return; } } if (fileCount === 0 && !$h.isEmpty(fileExt) && $h.isArray(fileExt) && !$h.isEmpty(fileExtExpr)) { chk = $h.compare(caption, fileExtExpr); fileCount += $h.isEmpty(chk) ? 0 : chk.length; if (fileCount === 0) { msg = self.msgInvalidFileExtension.setTokens({'name': caption, 'extensions': strExt}); throwError(msg, file, previewId, i); return; } } if (!self.showPreview) { if (self.isAjaxUpload) { self.addToStack(file); } setTimeout(function () { readFile(i + 1); self._updateFileDetails(numFiles); }, 100); self._raise('fileloaded', [file, previewId, i, reader]); return; } if (!canPreview && fileSize > maxPreviewSize) { self.addToStack(file); $container.addClass('file-thumb-loading'); self._previewDefault(file, previewId); self._initFileActions(); self._updateFileDetails(numFiles); readFile(i + 1); return; } if ($preview.length && FileReader !== undefined) { isText = fnText(file.type, caption); isHtml = fnHtml(file.type, caption); isImage = fnImage(file.type, caption); $status.html(msgLoading.replace('{index}', i + 1).replace('{files}', numFiles)); $container.addClass('file-thumb-loading'); reader.onerror = function (evt) { self._errorHandler(evt, caption); }; reader.onload = function (theFile) { var hex, fileInfo, uint, byte, bytes = [], contents, mime, readTextImage = function (textFlag) { var newReader = new FileReader(); newReader.onerror = function (theFileNew) { self._errorHandler(theFileNew, caption); }; newReader.onload = function (theFileNew) { self._previewFile(i, file, theFileNew, previewId, previewData, fileInfo); self._initFileActions(); processFileLoaded(); }; if (textFlag) { newReader.readAsText(file, self.textEncoding); } else { newReader.readAsDataURL(file); } }; fileInfo = {'name': caption, 'type': file.type}; $.each(settings, function (key, func) { if (key !== 'object' && key !== 'other' && func(file.type, caption)) { knownTypes++; } }); if (knownTypes === 0) {// auto detect mime types from content if no known file types detected uint = new Uint8Array(theFile.target.result); for (j = 0; j < uint.length; j++) { byte = uint[j].toString(16); bytes.push(byte); } hex = bytes.join('').toLowerCase().substring(0, 8); mime = $h.getMimeType(hex, '', ''); if ($h.isEmpty(mime)) { // look for ascii text content contents = $h.arrayBuffer2String(reader.result); mime = $h.isSvg(contents) ? 'image/svg+xml' : $h.getMimeType(hex, contents, file.type); } fileInfo = {'name': caption, 'type': mime}; isText = fnText(mime, ''); isHtml = fnHtml(mime, ''); isImage = fnImage(mime, ''); txtFlag = isText || isHtml; if (txtFlag || isImage) { readTextImage(txtFlag); return; } } self._previewFile(i, file, theFile, previewId, previewData, fileInfo); self._initFileActions(); processFileLoaded(); }; reader.onprogress = function (data) { if (data.lengthComputable) { var fact = (data.loaded / data.total) * 100, progress = Math.ceil(fact); msg = msgProgress.setTokens({ 'index': i + 1, 'files': numFiles, 'percent': progress, 'name': caption }); setTimeout(function () { $status.html(msg); }, 100); } }; if (isText || isHtml) { reader.readAsText(file, self.textEncoding); } else { if (isImage) { reader.readAsDataURL(file); } else { reader.readAsArrayBuffer(file); } } } else { self._previewDefault(file, previewId); setTimeout(function () { readFile(i + 1); self._updateFileDetails(numFiles); }, 100); self._raise('fileloaded', [file, previewId, i, reader]); } self.addToStack(file); }; readFile(0); self._updateFileDetails(numFiles, false); }, lock: function () { var self = this; self._resetErrors(); self.disable(); if (self.showRemove) { self.$container.find('.fileinput-remove').hide(); } if (self.showCancel) { self.$container.find('.fileinput-cancel').show(); } self._raise('filelock', [self.filestack, self._getExtraData()]); return self.$element; }, unlock: function (reset) { var self = this; if (reset === undefined) { reset = true; } self.enable(); if (self.showCancel) { self.$container.find('.fileinput-cancel').hide(); } if (self.showRemove) { self.$container.find('.fileinput-remove').show(); } if (reset) { self._resetFileStack(); } self._raise('fileunlock', [self.filestack, self._getExtraData()]); return self.$element; }, cancel: function () { var self = this, xhr = self.ajaxRequests, len = xhr.length, i; if (len > 0) { for (i = 0; i < len; i += 1) { self.cancelling = true; xhr[i].abort(); } } self._setProgressCancelled(); self._getThumbs().each(function () { var $thumb = $(this), ind = $thumb.attr('data-fileindex'); $thumb.removeClass('file-uploading'); if (self.filestack[ind] !== undefined) { $thumb.find('.kv-file-upload').removeClass('disabled').removeAttr('disabled'); $thumb.find('.kv-file-remove').removeClass('disabled').removeAttr('disabled'); } self.unlock(); }); return self.$element; }, clear: function () { var self = this, cap; if (!self._raise('fileclear')) { return; } self.$btnUpload.removeAttr('disabled'); self._getThumbs().find('video,audio,img').each(function () { $h.cleanMemory($(this)); }); self._resetUpload(); self.clearStack(); self._clearFileInput(); self._resetErrors(true); if (self._hasInitialPreview()) { self._showFileIcon(); self._resetPreview(); self._initPreviewActions(); self.$container.removeClass('file-input-new'); } else { self._getThumbs().each(function () { self._clearObjects($(this)); }); if (self.isAjaxUpload) { self.previewCache.data = {}; } self.$preview.html(''); cap = (!self.overwriteInitial && self.initialCaption.length > 0) ? self.initialCaption : ''; self.$caption.attr('title', '').val(cap); $h.addCss(self.$container, 'file-input-new'); self._validateDefaultPreview(); } if (self.$container.find($h.FRAMES).length === 0) { if (!self._initCaption()) { self.$captionContainer.removeClass('icon-visible'); } } self._hideFileIcon(); self._raise('filecleared'); self.$captionContainer.focus(); self._setFileDropZoneTitle(); return self.$element; }, reset: function () { var self = this; if (!self._raise('filereset')) { return; } self._resetPreview(); self.$container.find('.fileinput-filename').text(''); $h.addCss(self.$container, 'file-input-new'); if (self.getFrames().length || self.isAjaxUpload && self.dropZoneEnabled) { self.$container.removeClass('file-input-new'); } self.clearStack(); self.formdata = {}; self._setFileDropZoneTitle(); return self.$element; }, disable: function () { var self = this; self.isDisabled = true; self._raise('filedisabled'); self.$element.attr('disabled', 'disabled'); self.$container.find(".kv-fileinput-caption").addClass("file-caption-disabled"); self.$container.find(".fileinput-remove, .fileinput-upload, .file-preview-frame button") .attr("disabled", true); $h.addCss(self.$container.find('.btn-file'), 'disabled'); self._initDragDrop(); return self.$element; }, enable: function () { var self = this; self.isDisabled = false; self._raise('fileenabled'); self.$element.removeAttr('disabled'); self.$container.find(".kv-fileinput-caption").removeClass("file-caption-disabled"); self.$container.find(".fileinput-remove, .fileinput-upload, .file-preview-frame button") .removeAttr("disabled"); self.$container.find('.btn-file').removeClass('disabled'); self._initDragDrop(); return self.$element; }, upload: function () { var self = this, totLen = self.getFileStack().length, i, outData, len, hasExtraData = !$.isEmptyObject(self._getExtraData()); if (!self.isAjaxUpload || self.isDisabled || !self._isFileSelectionValid(totLen)) { return; } self._resetUpload(); if (totLen === 0 && !hasExtraData) { self._showUploadError(self.msgUploadEmpty); return; } self.$progress.show(); self.uploadCount = 0; self.uploadStatus = {}; self.uploadLog = []; self.lock(); self._setProgress(2); if (totLen === 0 && hasExtraData) { self._uploadExtraOnly(); return; } len = self.filestack.length; self.hasInitData = false; if (self.uploadAsync) { outData = self._getOutData(); self._raise('filebatchpreupload', [outData]); self.fileBatchCompleted = false; self.uploadCache = {content: [], config: [], tags: [], append: true}; self.uploadAsyncCount = self.getFileStack().length; for (i = 0; i < len; i++) { self.uploadCache.content[i] = null; self.uploadCache.config[i] = null; self.uploadCache.tags[i] = null; } self.$preview.find('.file-preview-initial').removeClass($h.SORT_CSS); self._initSortable(); self.cacheInitialPreview = self.getPreview(); for (i = 0; i < len; i++) { if (self.filestack[i]) { self._uploadSingle(i, true); } } return; } self._uploadBatch(); return self.$element; }, destroy: function () { var self = this, $form = self.$form, $cont = self.$container, $el = self.$element, ns = self.namespace; $(document).off(ns); $(window).off(ns); if ($form && $form.length) { $form.off(ns); } if (self.isAjaxUpload) { self._clearFileInput(); } self._cleanup(); self._initPreviewCache(); $el.insertBefore($cont).off(ns).removeData(); $cont.off().remove(); return $el; }, refresh: function (options, triggerChange) { var self = this, $el = self.$element; if (typeof options !== 'object' || $h.isEmpty(options)) { options = self.options; } else { options = $.extend(true, {}, self.options, options); } self._init(options, true); self._listen(); if (triggerChange) { $el.trigger('change' + self.namespace); } return $el; }, zoom: function (frameId) { var self = this, $frame = self._getFrame(frameId), $modal = self.$modal; if (!$frame) { return; } $h.initModal($modal); $modal.html(self._getModalContent()); self._setZoomContent($frame); $modal.modal('show'); self._initZoomButtons(); }, getExif: function (frameId) { var self = this, $frame = self._getFrame(frameId); return $frame && $frame.data('exif') || null; }, getFrames: function (cssFilter) { var self = this; cssFilter = cssFilter || ''; return self.$preview.find($h.FRAMES + cssFilter); }, getPreview: function () { var self = this; return { content: self.initialPreview, config: self.initialPreviewConfig, tags: self.initialPreviewThumbTags }; } }; $.fn.fileinput = function (option) { if (!$h.hasFileAPISupport() && !$h.isIE(9)) { return; } var args = Array.apply(null, arguments), retvals = []; args.shift(); this.each(function () { var self = $(this), data = self.data('fileinput'), options = typeof option === 'object' && option, theme = options.theme || self.data('theme'), l = {}, t = {}, lang = options.language || self.data('language') || $.fn.fileinput.defaults.language || 'en', opt; if (!data) { if (theme) { t = $.fn.fileinputThemes[theme] || {}; } if (lang !== 'en' && !$h.isEmpty($.fn.fileinputLocales[lang])) { l = $.fn.fileinputLocales[lang] || {}; } opt = $.extend(true, {}, $.fn.fileinput.defaults, t, $.fn.fileinputLocales.en, l, options, self.data()); data = new FileInput(this, opt); self.data('fileinput', data); } if (typeof option === 'string') { retvals.push(data[option].apply(data, args)); } }); switch (retvals.length) { case 0: return this; case 1: return retvals[0]; default: return retvals; } }; $.fn.fileinput.defaults = { language: 'en', showCaption: true, showBrowse: true, showPreview: true, showRemove: true, showUpload: true, showCancel: true, showClose: true, showUploadedThumbs: true, browseOnZoneClick: false, autoReplace: false, autoOrientImage: true, // for JPEG images based on EXIF orientation tag required: false, rtl: false, hideThumbnailContent: false, generateFileId: null, previewClass: '', captionClass: '', frameClass: 'krajee-default', mainClass: 'file-caption-main', mainTemplate: null, purifyHtml: true, fileSizeGetter: null, initialCaption: '', initialPreview: [], initialPreviewDelimiter: '*$$*', initialPreviewAsData: false, initialPreviewFileType: 'image', initialPreviewConfig: [], initialPreviewThumbTags: [], previewThumbTags: {}, initialPreviewShowDelete: true, initialPreviewDownloadUrl: '', removeFromPreviewOnError: false, deleteUrl: '', deleteExtraData: {}, overwriteInitial: true, previewZoomButtonIcons: { prev: '', next: '', toggleheader: '', fullscreen: '', borderless: '', close: '' }, previewZoomButtonClasses: { prev: 'btn btn-navigate', next: 'btn btn-navigate', toggleheader: 'btn btn-kv btn-default btn-outline-secondary', fullscreen: 'btn btn-kv btn-default btn-outline-secondary', borderless: 'btn btn-kv btn-default btn-outline-secondary', close: 'btn btn-kv btn-default btn-outline-secondary' }, preferIconicPreview: false, preferIconicZoomPreview: false, allowedPreviewTypes: undefined, allowedPreviewMimeTypes: null, allowedFileTypes: null, allowedFileExtensions: null, defaultPreviewContent: null, customLayoutTags: {}, customPreviewTags: {}, previewFileIcon: '', previewFileIconClass: 'file-other-icon', previewFileIconSettings: {}, previewFileExtSettings: {}, buttonLabelClass: 'hidden-xs', browseIcon: ' ', browseClass: 'btn btn-primary', removeIcon: '', removeClass: 'btn btn-default btn-secondary', cancelIcon: '', cancelClass: 'btn btn-default btn-secondary', uploadIcon: '', uploadClass: 'btn btn-default btn-secondary', uploadUrl: null, uploadUrlThumb: null, uploadAsync: true, uploadExtraData: {}, zoomModalHeight: 480, minImageWidth: null, minImageHeight: null, maxImageWidth: null, maxImageHeight: null, resizeImage: false, resizePreference: 'width', resizeQuality: 0.92, resizeDefaultImageType: 'image/jpeg', resizeIfSizeMoreThan: 0, // in KB minFileSize: 0, maxFileSize: 0, maxFilePreviewSize: 25600, // 25 MB minFileCount: 0, maxFileCount: 0, validateInitialCount: false, msgValidationErrorClass: 'text-danger', msgValidationErrorIcon: ' ', msgErrorClass: 'file-error-message', progressThumbClass: "progress-bar bg-success progress-bar-success progress-bar-striped active", progressClass: "progress-bar bg-success progress-bar-success progress-bar-striped active", progressCompleteClass: "progress-bar bg-success progress-bar-success", progressErrorClass: "progress-bar bg-danger progress-bar-danger", progressUploadThreshold: 99, previewFileType: 'image', elCaptionContainer: null, elCaptionText: null, elPreviewContainer: null, elPreviewImage: null, elPreviewStatus: null, elErrorContainer: null, errorCloseButton: $h.closeButton('kv-error-close'), slugCallback: null, dropZoneEnabled: true, dropZoneTitleClass: 'file-drop-zone-title', fileActionSettings: {}, otherActionButtons: '', textEncoding: 'UTF-8', ajaxSettings: {}, ajaxDeleteSettings: {}, showAjaxErrorDetails: true, mergeAjaxCallbacks: false, mergeAjaxDeleteCallbacks: false, retryErrorUploads: true }; $.fn.fileinputLocales.en = { fileSingle: 'file', filePlural: 'files', browseLabel: 'Browse …', removeLabel: 'Remove', removeTitle: 'Clear selected files', cancelLabel: 'Cancel', cancelTitle: 'Abort ongoing upload', uploadLabel: 'Upload', uploadTitle: 'Upload selected files', msgNo: 'No', msgNoFilesSelected: 'No files selected', msgCancelled: 'Cancelled', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detailed Preview', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'File "{name}" ({size} KB) exceeds maximum allowed upload size of {maxSize} KB.', msgFilesTooLess: 'You must select at least {n} {files} to upload.', msgFilesTooMany: 'Number of files selected for upload ({n}) exceeds maximum allowed limit of {m}.', msgFileNotFound: 'File "{name}" not found!', msgFileSecured: 'Security restrictions prevent reading the file "{name}".', msgFileNotReadable: 'File "{name}" is not readable.', msgFilePreviewAborted: 'File preview aborted for "{name}".', msgFilePreviewError: 'An error occurred while reading the file "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Invalid type for file "{name}". Only "{types}" files are supported.', msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'The file upload was aborted', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Validation Error', msgLoading: 'Loading file {index} of {files} …', msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.', msgSelected: '{n} {files} selected', msgFoldersNotAllowed: 'Drag & drop files only! {n} folder(s) dropped were skipped.', msgImageWidthSmall: 'Width of image file "{name}" must be at least {size} px.', msgImageHeightSmall: 'Height of image file "{name}" must be at least {size} px.', msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.', msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.', msgImageResizeError: 'Could not get the image dimensions to resize.', msgImageResizeException: 'Error while resizing the image.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & drop files here …', dropZoneClickTitle: '
    (or click to select {files})', previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; $.fn.fileinput.Constructor = FileInput; /** * Convert automatically file inputs with class 'file' into a bootstrap fileinput control. */ $(document).ready(function () { var $input = $('input.file[type=file]'); if ($input.length) { $input.fileinput(); } }); })); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/LANG.js ================================================ /*! * FileInput <_LANG_> Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['_LANG_'] = { fileSingle: 'file', filePlural: 'files', browseLabel: 'Browse …', removeLabel: 'Remove', removeTitle: 'Clear selected files', cancelLabel: 'Cancel', cancelTitle: 'Abort ongoing upload', uploadLabel: 'Upload', uploadTitle: 'Upload selected files', msgNo: 'No', msgNoFilesSelected: 'No files selected', msgCancelled: 'Cancelled', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detailed Preview', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'File "{name}" ({size} KB) exceeds maximum allowed upload size of {maxSize} KB.', msgFilesTooLess: 'You must select at least {n} {files} to upload.', msgFilesTooMany: 'Number of files selected for upload ({n}) exceeds maximum allowed limit of {m}.', msgFileNotFound: 'File "{name}" not found!', msgFileSecured: 'Security restrictions prevent reading the file "{name}".', msgFileNotReadable: 'File "{name}" is not readable.', msgFilePreviewAborted: 'File preview aborted for "{name}".', msgFilePreviewError: 'An error occurred while reading the file "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Invalid type for file "{name}". Only "{types}" files are supported.', msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'The file upload was aborted', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Validation Error', msgLoading: 'Loading file {index} of {files} …', msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.', msgSelected: '{n} {files} selected', msgFoldersNotAllowed: 'Drag & drop files only! Skipped {n} dropped folder(s).', msgImageWidthSmall: 'Width of image file "{name}" must be at least {size} px.', msgImageHeightSmall: 'Height of image file "{name}" must be at least {size} px.', msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.', msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.', msgImageResizeError: 'Could not get the image dimensions to resize.', msgImageResizeException: 'Error while resizing the image.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & drop files here …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Remove file', uploadTitle: 'Upload file', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'View details', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Not uploaded yet', indicatorSuccessTitle: 'Uploaded', indicatorErrorTitle: 'Upload Error', indicatorLoadingTitle: 'Uploading ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ar.js ================================================ /*! * FileInput Arabic Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Yasser Lotfy * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['ar'] = { fileSingle: 'ملف', filePlural: 'ملفات', browseLabel: 'تصفح …', removeLabel: 'إزالة', removeTitle: 'إزالة الملفات المختارة', cancelLabel: 'إلغاء', cancelTitle: 'إنهاء الرفع الحالي', uploadLabel: 'رفع', uploadTitle: 'رفع الملفات المختارة', msgNo: 'لا', msgNoFilesSelected: '', msgCancelled: 'ألغيت', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'معاينة تفصيلية', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'الملف "{name}" ({size} ك.ب) تعدى الحد الأقصى المسموح للرفع {maxSize} ك.ب.', msgFilesTooLess: 'يجب عليك اختيار {n} {files} على الأقل للرفع.', msgFilesTooMany: 'عدد الملفات المختارة للرفع ({n}) تعدت الحد الأقصى المسموح به لعدد {m}.', msgFileNotFound: 'الملف "{name}" غير موجود!', msgFileSecured: 'قيود أمنية تمنع قراءة الملف "{name}".', msgFileNotReadable: 'الملف "{name}" غير قابل للقراءة.', msgFilePreviewAborted: 'تم إلغاء معاينة الملف "{name}".', msgFilePreviewError: 'حدث خطأ أثناء قراءة الملف "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'نوعية غير صالحة للملف "{name}". فقط هذه النوعيات مدعومة "{types}".', msgInvalidFileExtension: 'امتداد غير صالح للملف "{name}". فقط هذه الملفات مدعومة "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'تم إلغاء رفع الملف', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'خطأ التحقق من صحة', msgLoading: 'تحميل ملف {index} من {files} …', msgProgress: 'تحميل ملف {index} من {files} - {name} - {percent}% منتهي.', msgSelected: '{n} {files} مختار(ة)', msgFoldersNotAllowed: 'اسحب وأفلت الملفات فقط! تم تخطي {n} مجلد(ات).', msgImageWidthSmall: 'عرض ملف الصورة "{name}" يجب أن يكون على الأقل {size} px.', msgImageHeightSmall: 'طول ملف الصورة "{name}" يجب أن يكون على الأقل {size} px.', msgImageWidthLarge: 'عرض ملف الصورة "{name}" لا يمكن أن يتعدى {size} px.', msgImageHeightLarge: 'طول ملف الصورة "{name}" لا يمكن أن يتعدى {size} px.', msgImageResizeError: 'لم يتمكن من معرفة أبعاد الصورة لتغييرها.', msgImageResizeException: 'حدث خطأ أثناء تغيير أبعاد الصورة.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'اسحب وأفلت الملفات هنا …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'إزالة الملف', uploadTitle: 'رفع الملف', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'مشاهدة التفاصيل', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'لم يتم الرفع بعد', indicatorSuccessTitle: 'تم الرفع', indicatorErrorTitle: 'خطأ بالرفع', indicatorLoadingTitle: 'جارٍ الرفع ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/az.js ================================================ /*! * FileInput Azerbaijan Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Elbrus * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['az'] = { fileSingle: 'fayl', filePlural: 'fayl', browseLabel: 'Seç …', removeLabel: 'Sil', removeTitle: 'Seçilmiş faylları təmizlə', cancelLabel: 'İmtina et', cancelTitle: 'Cari yükləməni dayandır', uploadLabel: 'Yüklə', uploadTitle: 'Seçilmiş faylları yüklə', msgNo: 'xeyir', msgNoFilesSelected: 'Heç bir fayl seçilməmişdir', msgCancelled: 'İmtina edildi', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'İlkin baxış', msgFileRequired: 'Yükləmə üçün fayl seçməlisiniz.', msgSizeTooSmall: 'Seçdiyiniz "{name}" faylının həcmi ({size} KB)-dır, minimum {minSize} KB olmalıdır.', msgSizeTooLarge: 'Seçdiyiniz "{name}" faylının həcmi ({size} KB)-dır, maksimum {maxSize} KB olmalıdır.', msgFilesTooLess: 'Yükləmə üçün minimum {n} {files} seçməlisiniz.', msgFilesTooMany: 'Seçilmiş fayl sayı ({n}). Maksimum {m} fayl seçmək mümkündür.', msgFileNotFound: 'Fayl "{name}" tapılmadı!', msgFileSecured: '"{name}" faylının istifadəsinə yetginiz yoxdur.', msgFileNotReadable: '"{name}" faylının istifadəsi mümkün deyil.', msgFilePreviewAborted: '"{name}" faylı üçün ilkin baxış ləğv olunub.', msgFilePreviewError: '"{name}" faylının oxunması mümkün olmadı.', msgInvalidFileName: '"{name}" faylının adında qadağan olunmuş simvollardan istifadə olunmuşdur.', msgInvalidFileType: '"{name}" faylının tipi dəstəklənmir. Yalnız "{types}" tipli faylları yükləmək mümkündür.', msgInvalidFileExtension: '"{name}" faylının genişlənməsi yanlışdır. Yalnız "{extensions}" fayl genişlənmə(si / ləri) qəbul olunur.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Yükləmə dayandırılmışdır', msgUploadThreshold: 'Yükləmə...', msgUploadBegin: 'Yoxlama...', msgUploadEnd: 'Fayl(lar) yükləndi', msgUploadEmpty: 'Yükləmə üçün verilmiş məlumatlar yanlışdır', msgUploadError: 'Error', msgValidationError: 'Yoxlama nəticəsi səhvir', msgLoading: '{files} fayldan {index} yüklənir …', msgProgress: '{files} fayldan {index} - {name} - {percent}% yükləndi.', msgSelected: 'Faylların sayı: {n}', msgFoldersNotAllowed: 'Ancaq faylların daşınmasına icazə verilir! {n} qovluq yüklənmədi.', msgImageWidthSmall: '{name} faylının eni {size} px -dən kiçik olmamalıdır.', msgImageHeightSmall: '{name} faylının hündürlüyü {size} px -dən kiçik olmamalıdır.', msgImageWidthLarge: '"{name}" faylının eni {size} px -dən böyük olmamalıdır.', msgImageHeightLarge: '"{name}" faylının hündürlüyü {size} px -dən böyük olmamalıdır.', msgImageResizeError: 'Faylın ölçülərini dəyişmək üçün ölçüləri hesablamaq mümkün olmadı.', msgImageResizeException: 'Faylın ölçülərini dəyişmək mümkün olmadı.
    {errors}
    ', msgAjaxError: '{operation} əməliyyatı zamanı səhv baş verdi. Təkrar yoxlayın!', msgAjaxProgressError: '{operation} əməliyyatı yerinə yetirmək mümkün olmadı.', ajaxOperations: { deleteThumb: 'faylı sil', uploadThumb: 'faylı yüklə', uploadBatch: 'bir neçə faylı yüklə', uploadExtra: 'məlumatların yüklənməsi' }, dropZoneTitle: 'Faylları bura daşıyın …', dropZoneClickTitle: '
    (Və ya seçin {files})', fileActionSettings: { removeTitle: 'Faylı sil', uploadTitle: 'Faylı yüklə', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'məlumatlara bax', dragTitle: 'Yerini dəyiş və ya sırala', indicatorNewTitle: 'Davam edir', indicatorSuccessTitle: 'Tamamlandı', indicatorErrorTitle: 'Yükləmə xətası', indicatorLoadingTitle: 'Yükləmə ...' }, previewZoomButtonTitles: { prev: 'Əvvəlki fayla bax', next: 'Növbəti fayla bax', toggleheader: 'Başlığı dəyiş', fullscreen: 'Tam ekranı dəyiş', borderless: 'Bölmələrsiz rejimi dəyiş', close: 'Ətraflı baxışı bağla' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/bg.js ================================================ /*! * FileInput Bulgarian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['bg'] = { fileSingle: 'файл', filePlural: 'файла', browseLabel: 'Избери …', removeLabel: 'Премахни', removeTitle: 'Изчисти избраните', cancelLabel: 'Откажи', cancelTitle: 'Откажи качването', uploadLabel: 'Качи', uploadTitle: 'Качи избраните файлове', msgNo: 'Не', msgNoFilesSelected: '', msgCancelled: 'Отменен', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Детайлен преглед', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Файла "{name}" ({size} KB) надвишава максималните разрешени {maxSize} KB.', msgFilesTooLess: 'Трябва да изберете поне {n} {files} файла.', msgFilesTooMany: 'Броя файлове избрани за качване ({n}) надвишава ограниченито от максимум {m}.', msgFileNotFound: 'Файлът "{name}" не може да бъде намерен!', msgFileSecured: 'От съображения за сигурност не може да прочетем файла "{name}".', msgFileNotReadable: 'Файлът "{name}" не е четим.', msgFilePreviewAborted: 'Прегледа на файла е прекратен за "{name}".', msgFilePreviewError: 'Грешка при опит за четене на файла "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Невалиден тип на файла "{name}". Разрешени са само "{types}".', msgInvalidFileExtension: 'Невалидно разрешение на "{name}". Разрешени са само "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Качите файла, бе прекратена', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'утвърждаване грешка', msgLoading: 'Зареждане на файл {index} от общо {files} …', msgProgress: 'Зареждане на файл {index} от общо {files} - {name} - {percent}% завършени.', msgSelected: '{n} {files} избрани', msgFoldersNotAllowed: 'Само пуснати файлове! Пропуснати {n} пуснати папки.', msgImageWidthSmall: 'Широчината на изображението "{name}" трябва да е поне {size} px.', msgImageHeightSmall: 'Височината на изображението "{name}" трябва да е поне {size} px.', msgImageWidthLarge: 'Широчината на изображението "{name}" не може да е по-голяма от {size} px.', msgImageHeightLarge: 'Височината на изображението "{name}" нее може да е по-голяма от {size} px.', msgImageResizeError: 'Не може да размерите на изображението, за да промените размера.', msgImageResizeException: 'Грешка при промяна на размера на изображението.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Пуснете файловете тук …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Махни файл', uploadTitle: 'Качване на файл', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Вижте детайли', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Все още не е качил', indicatorSuccessTitle: 'Качено', indicatorErrorTitle: 'Качи Error', indicatorLoadingTitle: 'Качва се ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ca.js ================================================ /*! * FileInput Català Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['ca'] = { fileSingle: 'arxiu', filePlural: 'arxius', browseLabel: 'Examinar …', removeLabel: 'Treure', removeTitle: 'Treure arxius seleccionats', cancelLabel: 'Cancel', cancelTitle: 'Avortar la pujada en curs', uploadLabel: 'Pujar arxiu', uploadTitle: 'Pujar arxius seleccionats', msgNo: 'No', msgNoFilesSelected: '', msgCancelled: 'cancel·lat', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Vista prèvia detallada', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Arxiu "{name}" ({size} KB) excedeix la mida màxima permès de {maxSize} KB.', msgFilesTooLess: 'Heu de seleccionar almenys {n} {files} a carregar.', msgFilesTooMany: 'El nombre d\'arxius seleccionats a carregar ({n}) excedeix el límit màxim permès de {m}.', msgFileNotFound: 'Arxiu "{name}" no trobat.', msgFileSecured: 'No es pot accedir a l\'arxiu "{name}" perquè estarà sent usat per una altra aplicació o no tinguem permisos de lectura.', msgFileNotReadable: 'No es pot accedir a l\'arxiu "{name}".', msgFilePreviewAborted: 'Previsualització de l\'arxiu "{name}" cancel·lada.', msgFilePreviewError: 'S\'ha produït un error mentre es llegia el fitxer "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Tipus de fitxer no vàlid per a "{name}". Només arxius "{types}" són permesos.', msgInvalidFileExtension: 'Extensió de fitxer no vàlid per a "{name}". Només arxius "{extensions}" són permesos.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'La càrrega d\'arxius s\'ha cancel·lat', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Error de validació', msgLoading: 'Pujant fitxer {index} de {files} …', msgProgress: 'Pujant fitxer {index} de {files} - {name} - {percent}% completat.', msgSelected: '{n} {files} seleccionat(s)', msgFoldersNotAllowed: 'Arrossegueu i deixeu anar únicament arxius. Omesa(es) {n} carpeta(es).', msgImageWidthSmall: 'L\'ample de la imatge "{name}" ha de ser almenys {size} px.', msgImageHeightSmall: 'L\'alçada de la imatge "{name}" ha de ser almenys {size} px.', msgImageWidthLarge: 'L\'ample de la imatge "{name}" no pot excedir de {size} px.', msgImageHeightLarge: 'L\'alçada de la imatge "{name}" no pot excedir de {size} px.', msgImageResizeError: 'No s\'ha pogut obtenir les dimensions d\'imatge per canviar la mida.', msgImageResizeException: 'Error en canviar la mida de la imatge.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Arrossegueu i deixeu anar aquí els arxius …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Eliminar arxiu', uploadTitle: 'Pujar arxiu', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Veure detalls', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'No pujat encara', indicatorSuccessTitle: 'Subido', indicatorErrorTitle: 'Pujar Error', indicatorLoadingTitle: 'Pujant ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/cr.js ================================================ /*! * FileInput Croatian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Milos Stojanovic * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['cr'] = { fileSingle: 'datoteka', filePlural: 'datoteke', browseLabel: 'Izaberi …', removeLabel: 'Ukloni', removeTitle: 'Ukloni označene datoteke', cancelLabel: 'Odustani', cancelTitle: 'Prekini trenutno otpremanje', uploadLabel: 'Otpremi', uploadTitle: 'Otpremi označene datoteke', msgNo: 'Ne', msgNoFilesSelected: '', msgCancelled: 'Otkazan', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detaljni pregled', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Datoteka "{name}" ({size} KB) prekoračuje maksimalnu dozvoljenu veličinu datoteke od {maxSize} KB.', msgFilesTooLess: 'Morate odabrati najmanje {n} {files} za otpremanje.', msgFilesTooMany: 'Broj datoteka označenih za otpremanje ({n}) prekoračuje maksimalni dozvoljeni limit od {m}.', msgFileNotFound: 'Datoteka "{name}" nije pronađena!', msgFileSecured: 'Datoteku "{name}" nije moguće pročitati zbog bezbednosnih ograničenja.', msgFileNotReadable: 'Datoteku "{name}" nije moguće pročitati.', msgFilePreviewAborted: 'Generisanje prikaza nije moguće za "{name}".', msgFilePreviewError: 'Došlo je do greške prilikom čitanja datoteke "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Datoteka "{name}" je pogrešnog formata. Dozvoljeni formati su "{types}".', msgInvalidFileExtension: 'Ekstenzija datoteke "{name}" nije dozvoljena. Dozvoljene ekstenzije su "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Prijenos datoteka je prekinut', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Provjera pogrešaka', msgLoading: 'Učitavanje datoteke {index} od {files} …', msgProgress: 'Učitavanje datoteke {index} od {files} - {name} - {percent}% završeno.', msgSelected: '{n} {files} je označeno', msgFoldersNotAllowed: 'Moguće je prevlačiti samo datoteke! Preskočeno je {n} fascikla.', msgImageWidthSmall: 'Širina slikovnu datoteku "{name}" moraju biti najmanje {size} px.', msgImageHeightSmall: 'Visina slikovnu datoteku "{name}" moraju biti najmanje {size} px.', msgImageWidthLarge: 'Širina slikovnu datoteku "{name}" ne može prelaziti {size} px.', msgImageHeightLarge: 'Visina slikovnu datoteku "{name}" ne može prelaziti {size} px.', msgImageResizeError: 'Nije mogao dobiti dimenzije slike na veličinu.', msgImageResizeException: 'Greška prilikom promjene veličine slike.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Prevucite datoteke ovde …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Uklonite datoteku', uploadTitle: 'Postavi datoteku', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Pregledavati pojedinosti', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Još nije učitao', indicatorSuccessTitle: 'Preneseno', indicatorErrorTitle: 'Postavi Greška', indicatorLoadingTitle: 'Prijenos ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/cs.js ================================================ /*! * FileInput Czech Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['cs'] = { fileSingle: 'soubor', filePlural: 'soubory', browseLabel: 'Vybrat …', removeLabel: 'Odstranit', removeTitle: 'Vyčistit vybrané soubory', cancelLabel: 'Storno', cancelTitle: 'Přerušit nahrávání', uploadLabel: 'Nahrát', uploadTitle: 'Nahrát vybrané soubory', msgNo: 'Ne', msgNoFilesSelected: 'Nevybrány žádné soubory', msgCancelled: 'Zrušeno', msgPlaceholder: 'Vybrat {files}...', msgZoomModalHeading: 'Detailní náhled', msgFileRequired: 'Musíte vybrat soubor, který chcete nahrát.', msgSizeTooSmall: 'Soubor "{name}" ({size} KB) je příliš malý, musí mít velikost nejméně {minSize} KB.', msgSizeTooLarge: 'Soubor "{name}" ({size} KB) je příliš velký, maximální povolená velikost {maxSize} KB.', msgFilesTooLess: 'Musíte vybrat nejméně {n} {files} souborů.', msgFilesTooMany: 'Počet vybraných souborů ({n}) překročil maximální povolený limit {m}.', msgFileNotFound: 'Soubor "{name}" nebyl nalezen!', msgFileSecured: 'Zabezpečení souboru znemožnilo číst soubor "{name}".', msgFileNotReadable: 'Soubor "{name}" není čitelný.', msgFilePreviewAborted: 'Náhled souboru byl přerušen pro "{name}".', msgFilePreviewError: 'Nastala chyba při načtení souboru "{name}".', msgInvalidFileName: 'Neplatné nebo nepovolené znaky ve jménu souboru "{name}".', msgInvalidFileType: 'Neplatný typ souboru "{name}". Pouze "{types}" souborů jsou podporovány.', msgInvalidFileExtension: 'Neplatná extenze souboru "{name}". Pouze "{extensions}" souborů jsou podporovány.', msgFileTypes: { 'image': 'obrázek', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Nahrávání souboru bylo přerušeno', msgUploadThreshold: 'Zpracovávám...', msgUploadBegin: 'Inicializujem...', msgUploadEnd: 'Hotovo', msgUploadEmpty: 'Pro nahrávání nejsou k dispozici žádné platné údaje.', msgUploadError: 'Chyba', msgValidationError: 'Chyba ověření', msgLoading: 'Nahrávání souboru {index} z {files} …', msgProgress: 'Nahrávání souboru {index} z {files} - {name} - {percent}% dokončeno.', msgSelected: '{n} {files} vybráno', msgFoldersNotAllowed: 'Táhni a pusť pouze soubory! Vynechané {n} pustěné složk(y).', msgImageWidthSmall: 'Šířka obrázku "{name}", musí být alespoň {size} px.', msgImageHeightSmall: 'Výška obrázku "{name}", musí být alespoň {size} px.', msgImageWidthLarge: 'Šířka obrázku "{name}" nesmí být větší než {size} px.', msgImageHeightLarge: 'Výška obrázku "{name}" nesmí být větší než {size} px.', msgImageResizeError: 'Nelze získat rozměry obrázku pro změnu velikosti.', msgImageResizeException: 'Chyba při změně velikosti obrázku.
    {errors}
    ', msgAjaxError: 'Došlo k chybě v {operation}. Prosím zkuste to znovu později!', msgAjaxProgressError: '{operation} - neúspěšné', ajaxOperations: { deleteThumb: 'odstranit soubor', uploadThumb: 'nahrát soubor', uploadBatch: 'nahrát várku souborů', uploadExtra: 'odesílání dat formuláře' }, dropZoneTitle: 'Přetáhni soubory sem …', dropZoneClickTitle: '
    (nebo klikni sem a vyber je)', fileActionSettings: { removeTitle: 'Odstranit soubor', uploadTitle: 'Nahrát soubor', uploadRetryTitle: 'Opakovat nahrávání', downloadTitle: 'Stáhnout soubor', zoomTitle: 'Zobrazit podrobnosti', dragTitle: 'Posunout / Přeskládat', indicatorNewTitle: 'Ještě nenahrál', indicatorSuccessTitle: 'Nahraný', indicatorErrorTitle: 'Chyba nahrávání', indicatorLoadingTitle: 'Nahrávání ...' }, previewZoomButtonTitles: { prev: 'Zobrazit předchozí soubor', next: 'Zobrazit následující soubor', toggleheader: 'Přepnout záhlaví', fullscreen: 'Přepnout celoobrazovkové zobrazení', borderless: 'Přepnout bezrámečkové zobrazení', close: 'Zavřít detailní náhled' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/da.js ================================================ /*! * FileInput Danish Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['da'] = { fileSingle: 'fil', filePlural: 'filer', browseLabel: 'Browse …', removeLabel: 'Fjern', removeTitle: 'Fjern valgte filer', cancelLabel: 'Fortryd', cancelTitle: 'Afbryd nuværende upload', uploadLabel: 'Upload', uploadTitle: 'Upload valgte filer', msgNo: 'Ingen', msgNoFilesSelected: '', msgCancelled: 'aflyst', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detaljeret visning', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Fil "{name}" ({size} KB) er større end de tilladte {maxSize} KB.', msgFilesTooLess: 'Du skal mindst vælge {n} {files} til upload.', msgFilesTooMany: '({n}) filer valgt til upload, men maks. {m} er tilladt.', msgFileNotFound: 'Filen "{name}" blev ikke fundet!', msgFileSecured: 'Sikkerhedsrestriktioner forhindrer læsning af "{name}".', msgFileNotReadable: 'Filen "{name}" kan ikke indlæses.', msgFilePreviewAborted: 'Filpreview annulleret for "{name}".', msgFilePreviewError: 'Der skete en fejl under læsningen af filen "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Ukendt type for filen "{name}". Kun "{types}" kan bruges.', msgInvalidFileExtension: 'Ukendt filtype for filen "{name}". Kun "{extensions}" filer kan bruges.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Filupload annulleret', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Validering Fejl', msgLoading: 'Henter fil {index} af {files} …', msgProgress: 'Henter fil {index} af {files} - {name} - {percent}% færdiggjort.', msgSelected: '{n} {files} valgt', msgFoldersNotAllowed: 'Drag & drop kun filer! {n} mappe(r) sprunget over.', msgImageWidthSmall: 'Bredden af billedet "{name}" skal være på mindst {size} px.', msgImageHeightSmall: 'Højden af billedet "{name}" skal være på mindst {size} px.', msgImageWidthLarge: 'Bredden af billedet "{name}" må ikke være over {size} px.', msgImageHeightLarge: 'Højden af billedet "{name}" må ikke være over {size} px.', msgImageResizeError: 'Kunne ikke få billedets dimensioner for at ændre størrelsen.', msgImageResizeException: 'Fejl ved at ændre størrelsen på billedet.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & drop filer her …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Fjern fil', uploadTitle: 'Upload fil', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Se detaljer', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Ikke uploadet endnu', indicatorSuccessTitle: 'Uploadet', indicatorErrorTitle: 'Upload fejl', indicatorLoadingTitle: 'Uploader ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/de.js ================================================ /*! * FileInput German Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput */ (function ($) { "use strict"; $.fn.fileinputLocales['de'] = { fileSingle: 'Datei', filePlural: 'Dateien', browseLabel: 'Auswählen …', removeLabel: 'Löschen', removeTitle: 'Ausgewählte löschen', cancelLabel: 'Abbrechen', cancelTitle: 'Hochladen abbrechen', uploadLabel: 'Hochladen', uploadTitle: 'Hochladen der ausgewählten Dateien', msgNo: 'Keine', msgNoFilesSelected: 'Keine Dateien ausgewählt', msgCancelled: 'Abgebrochen', msgPlaceholder: '{files} auswählen...', msgZoomModalHeading: 'ausführliche Vorschau', msgFileRequired: 'Sie müssen eine Datei zum Hochladen auswählen.', msgSizeTooSmall: 'Datei "{name}" ({size} KB) unterschreitet mindestens notwendige Upload-Größe von {minSize} KB.', msgSizeTooLarge: 'Datei "{name}" ({size} KB) überschreitet maximal zulässige Upload-Größe von {maxSize} KB.', msgFilesTooLess: 'Sie müssen mindestens {n} {files} zum Hochladen auswählen.', msgFilesTooMany: 'Anzahl der zum Hochladen ausgewählten Dateien ({n}), überschreitet maximal zulässige Grenze von {m} Stück.', msgFileNotFound: 'Datei "{name}" wurde nicht gefunden!', msgFileSecured: 'Sicherheitseinstellungen verhindern das Lesen der Datei "{name}".', msgFileNotReadable: 'Die Datei "{name}" ist nicht lesbar.', msgFilePreviewAborted: 'Dateivorschau abgebrochen für "{name}".', msgFilePreviewError: 'Beim Lesen der Datei "{name}" ein Fehler aufgetreten.', msgInvalidFileName: 'Ungültige oder nicht unterstützte Zeichen im Dateinamen "{name}".', msgInvalidFileType: 'Ungültiger Typ für Datei "{name}". Nur Dateien der Typen "{types}" werden unterstützt.', msgInvalidFileExtension: 'Ungültige Erweiterung für Datei "{name}". Nur Dateien mit der Endung "{extensions}" werden unterstützt.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Der Datei-Upload wurde abgebrochen', msgUploadThreshold: 'Wird bearbeitet ...', msgUploadBegin: 'Wird initialisiert ...', msgUploadEnd: 'Erledigt', msgUploadEmpty: 'Keine gültigen Daten zum Hochladen verfügbar.', msgUploadError: 'Fehler', msgValidationError: 'Validierungsfehler', msgLoading: 'Lade Datei {index} von {files} hoch…', msgProgress: 'Datei {index} von {files} - {name} - zu {percent}% fertiggestellt.', msgSelected: '{n} {files} ausgewählt', msgFoldersNotAllowed: 'Drag & Drop funktioniert nur bei Dateien! {n} Ordner übersprungen.', msgImageWidthSmall: 'Breite der Bilddatei "{name}" muss mindestens {size} px betragen.', msgImageHeightSmall: 'Höhe der Bilddatei "{name}" muss mindestens {size} px betragen.', msgImageWidthLarge: 'Breite der Bilddatei "{name}" nicht überschreiten {size} px.', msgImageHeightLarge: 'Höhe der Bilddatei "{name}" nicht überschreiten {size} px.', msgImageResizeError: 'Konnte nicht die Bildabmessungen zu ändern.', msgImageResizeException: 'Fehler beim Ändern der Größe des Bildes.
    {errors}
    ', msgAjaxError: 'Bei der Aktion {operation} ist ein Fehler aufgetreten. Bitte versuche es später noch einmal!', msgAjaxProgressError: '{operation} fehlgeschlagen', ajaxOperations: { deleteThumb: 'Datei löschen', uploadThumb: 'Datei hochladen', uploadBatch: 'Batch-Datei-Upload', uploadExtra: 'Formular-Datei-Upload' }, dropZoneTitle: 'Dateien hierher ziehen …', dropZoneClickTitle: '
    (oder klicken um {files} auszuwählen)', fileActionSettings: { removeTitle: 'Datei entfernen', uploadTitle: 'Datei hochladen', uploadRetryTitle: 'Upload erneut versuchen', downloadTitle: 'Datei herunterladen', zoomTitle: 'Details anzeigen', dragTitle: 'Verschieben / Neuordnen', indicatorNewTitle: 'Noch nicht hochgeladen', indicatorSuccessTitle: 'Hochgeladen', indicatorErrorTitle: 'Upload Fehler', indicatorLoadingTitle: 'Hochladen ...' }, previewZoomButtonTitles: { prev: 'Vorherige Datei anzeigen', next: 'Nächste Datei anzeigen', toggleheader: 'Header umschalten', fullscreen: 'Vollbildmodus umschalten', borderless: 'Randlosen Modus umschalten', close: 'Detailansicht schließen' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/el.js ================================================ /*! * FileInput Greek Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['el'] = { fileSingle: 'αρχείο', filePlural: 'αρχεία', browseLabel: 'Αναζήτηση …', removeLabel: 'Διαγραφή', removeTitle: 'Εκκαθάριση αρχείων', cancelLabel: 'Ακύρωση', cancelTitle: 'Ακύρωση μεταφόρτωσης', uploadLabel: 'Μεταφόρτωση', uploadTitle: 'Μεταφόρτωση επιλεγμένων αρχείων', msgNo: 'Όχι', msgNoFilesSelected: 'Δεν επιλέχθηκαν αρχεία', msgCancelled: 'Ακυρώθηκε', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Λεπτομερής Προεπισκόπηση', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'Το "{name}" ({size} KB) είναι πολύ μικρό, πρέπει να είναι μεγαλύτερο από {minSize} KB.', msgSizeTooLarge: 'Το αρχείο "{name}" ({size} KB) υπερβαίνει το μέγιστο επιτρεπόμενο μέγεθος μεταφόρτωσης {maxSize} KB.', msgFilesTooLess: 'Πρέπει να επιλέξετε τουλάχιστον {n} {files} για να ξεκινήσει η μεταφόρτωση.', msgFilesTooMany: 'Ο αριθμός των αρχείων που έχουν επιλεγεί για μεταφόρτωση ({n}) υπερβαίνει το μέγιστο επιτρεπόμενο αριθμό {m}.', msgFileNotFound: 'Το αρχείο "{name}" δεν βρέθηκε!', msgFileSecured: 'Περιορισμοί ασφαλείας εμπόδισαν την ανάγνωση του αρχείου "{name}".', msgFileNotReadable: 'Το αρχείο "{name}" δεν είναι αναγνώσιμο.', msgFilePreviewAborted: 'Η προεπισκόπηση του αρχείου "{name}" ακυρώθηκε.', msgFilePreviewError: 'Παρουσιάστηκε σφάλμα κατά την ανάγνωση του αρχείου "{name}".', msgInvalidFileName: 'Μη έγκυροι χαρακτήρες στο όνομα του αρχείου "{name}".', msgInvalidFileType: 'Μη έγκυρος ο τύπος του αρχείου "{name}". Οι τύποι αρχείων που υποστηρίζονται είναι : "{types}".', msgInvalidFileExtension: 'Μη έγκυρη η επέκταση του αρχείου "{name}". Οι επεκτάσεις που υποστηρίζονται είναι : "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Η μεταφόρτωση του αρχείου ματαιώθηκε', msgUploadThreshold: 'Μεταφόρτωση ...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Σφάλμα Επικύρωσης', msgLoading: 'Φόρτωση αρχείου {index} από {files} …', msgProgress: 'Φόρτωση αρχείου {index} απο {files} - {name} - {percent}% ολοκληρώθηκε.', msgSelected: '{n} {files} επιλέχθηκαν', msgFoldersNotAllowed: 'Μπορείτε να σύρετε μόνο αρχεία! Παραβλέφθηκαν {n} φάκελος(οι).', msgImageWidthSmall: 'Το πλάτος του αρχείου εικόνας "{name}" πρέπει να είναι τουλάχιστον {size} px.', msgImageHeightSmall: 'Το ύψος του αρχείου εικόνας "{name}" πρέπει να είναι τουλάχιστον {size} px.', msgImageWidthLarge: 'Το πλάτος του αρχείου εικόνας "{name}" δεν μπορεί να υπερβαίνει το {size} px.', msgImageHeightLarge: 'Το ύψος του αρχείου εικόνας "{name}" δεν μπορεί να υπερβαίνει το {size} px.', msgImageResizeError: 'Δεν μπορούν να βρεθούν οι διαστάσεις της εικόνας για να αλλάγή μεγέθους.', msgImageResizeException: 'Σφάλμα κατά την αλλαγή μεγέθους της εικόνας.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Σύρετε τα αρχεία εδώ …', dropZoneClickTitle: '
    (ή πατήστε για επιλογή {files})', fileActionSettings: { removeTitle: 'Αφαιρέστε το αρχείο', uploadTitle: 'Μεταφορτώστε το αρχείο', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Δείτε λεπτομέρειες', dragTitle: 'Μετακίνηση/Προσπαρμογή', indicatorNewTitle: 'Δεν μεταφορτώθηκε ακόμα', indicatorSuccessTitle: 'Μεταφορτώθηκε', indicatorErrorTitle: 'Σφάλμα Μεταφόρτωσης', indicatorLoadingTitle: 'Μεταφόρτωση ...' }, previewZoomButtonTitles: { prev: 'Προηγούμενο αρχείο', next: 'Επόμενο αρχείο', toggleheader: 'Εμφάνιση/Απόκρυψη τίτλου', fullscreen: 'Εναλλαγή πλήρους οθόνης', borderless: 'Με ή χωρίς πλαίσιο', close: 'Κλείσιμο προβολής' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/es.js ================================================ /*! * FileInput Spanish Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['es'] = { fileSingle: 'archivo', filePlural: 'archivos', browseLabel: 'Examinar …', removeLabel: 'Quitar', removeTitle: 'Quitar archivos seleccionados', cancelLabel: 'Cancelar', cancelTitle: 'Abortar la subida en curso', uploadLabel: 'Subir archivo', uploadTitle: 'Subir archivos seleccionados', msgNo: 'No', msgNoFilesSelected: 'No hay archivos seleccionados', msgCancelled: 'Cancelado', msgPlaceholder: 'Seleccionar {files}...', msgZoomModalHeading: 'Vista previa detallada', msgFileRequired: 'Debes seleccionar un archivo para subir.', msgSizeTooSmall: 'El archivo "{name}" ({size} KB) es demasiado pequeño y debe ser mayor de {minSize} KB.', msgSizeTooLarge: 'El archivo "{name}" ({size} KB) excede el tamaño máximo permitido de {maxSize} KB.', msgFilesTooLess: 'Debe seleccionar al menos {n} {files} a cargar.', msgFilesTooMany: 'El número de archivos seleccionados a cargar ({n}) excede el límite máximo permitido de {m}.', msgFileNotFound: 'Archivo "{name}" no encontrado.', msgFileSecured: 'No es posible acceder al archivo "{name}" porque está siendo usado por otra aplicación o no tiene permisos de lectura.', msgFileNotReadable: 'No es posible acceder al archivo "{name}".', msgFilePreviewAborted: 'Previsualización del archivo "{name}" cancelada.', msgFilePreviewError: 'Ocurrió un error mientras se leía el archivo "{name}".', msgInvalidFileName: 'Caracteres no válidos o no soportados en el nombre del archivo "{name}".', msgInvalidFileType: 'Tipo de archivo no válido para "{name}". Sólo se permiten archivos de tipo "{types}".', msgInvalidFileExtension: 'Extensión de archivo no válida para "{name}". Sólo se permiten archivos "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'La carga de archivos se ha cancelado', msgUploadThreshold: 'Procesando...', msgUploadBegin: 'Inicializando...', msgUploadEnd: 'Hecho', msgUploadEmpty: 'No existen datos válidos para el envío.', msgUploadError: 'Error', msgValidationError: 'Error de validación', msgLoading: 'Subiendo archivo {index} de {files} …', msgProgress: 'Subiendo archivo {index} de {files} - {name} - {percent}% completado.', msgSelected: '{n} {files} seleccionado(s)', msgFoldersNotAllowed: 'Arrastre y suelte únicamente archivos. Omitida(s) {n} carpeta(s).', msgImageWidthSmall: 'El ancho de la imagen "{name}" debe ser de al menos {size} px.', msgImageHeightSmall: 'La altura de la imagen "{name}" debe ser de al menos {size} px.', msgImageWidthLarge: 'El ancho de la imagen "{name}" no puede exceder de {size} px.', msgImageHeightLarge: 'La altura de la imagen "{name}" no puede exceder de {size} px.', msgImageResizeError: 'No se pudieron obtener las dimensiones de la imagen para cambiar el tamaño.', msgImageResizeException: 'Error al cambiar el tamaño de la imagen.
    {errors}
    ', msgAjaxError: 'Algo ha ido mal con la operación {operation}. Por favor, inténtelo de nuevo mas tarde.', msgAjaxProgressError: 'La operación {operation} ha fallado', ajaxOperations: { deleteThumb: 'Archivo borrado', uploadThumb: 'Archivo subido', uploadBatch: 'Datos subidos en lote', uploadExtra: 'Datos del formulario subidos ' }, dropZoneTitle: 'Arrastre y suelte aquí los archivos …', dropZoneClickTitle: '
    (o haga clic para seleccionar {files})', fileActionSettings: { removeTitle: 'Eliminar archivo', uploadTitle: 'Subir archivo', uploadRetryTitle: 'Reintentar subir', downloadTitle: 'Descargar archivo', zoomTitle: 'Ver detalles', dragTitle: 'Mover / Reordenar', indicatorNewTitle: 'No subido todavía', indicatorSuccessTitle: 'Subido', indicatorErrorTitle: 'Error al subir', indicatorLoadingTitle: 'Subiendo...' }, previewZoomButtonTitles: { prev: 'Anterior', next: 'Siguiente', toggleheader: 'Mostrar encabezado', fullscreen: 'Pantalla completa', borderless: 'Modo sin bordes', close: 'Cerrar vista detallada' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/et.js ================================================ /*! * FileInput Estonian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['et'] = { fileSingle: 'fail', filePlural: 'failid', browseLabel: 'Sirvi …', removeLabel: 'Eemalda', removeTitle: 'Clear selected files', cancelLabel: 'Tühista', cancelTitle: 'Abort ongoing upload', uploadLabel: 'Salvesta', uploadTitle: 'Salvesta valitud failid', msgNo: 'No', msgNoFilesSelected: 'No files selected', msgCancelled: 'Cancelled', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detailed Preview', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Fail "{name}" ({size} KB) ületab lubatu suuruse {maxSize} KB.', msgFilesTooLess: 'You must select at least {n} {files} to upload.', msgFilesTooMany: 'Number of files selected for upload ({n}) exceeds maximum allowed limit of {m}.', msgFileNotFound: 'File "{name}" not found!', msgFileSecured: 'Security restrictions prevent reading the file "{name}".', msgFileNotReadable: 'File "{name}" is not readable.', msgFilePreviewAborted: 'File preview aborted for "{name}".', msgFilePreviewError: 'An error occurred while reading the file "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: '"{name}" on vale tüüpi. Ainult "{types}" on lubatud.', msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'The file upload was aborted', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Validation Error', msgLoading: 'Loading file {index} of {files} …', msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.', msgSelected: '{n} {files} selected', msgFoldersNotAllowed: 'Drag & drop files only! Skipped {n} dropped folder(s).', msgImageWidthSmall: 'Pildi laius peab olema vähemalt {size} px.', msgImageHeightSmall: 'Pildi kõrgus peab olema vähemalt {size} px.', msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.', msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.', msgImageResizeError: 'Could not get the image dimensions to resize.', msgImageResizeException: 'Error while resizing the image.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Lohista failid siia …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Eemalda fail', uploadTitle: 'Salvesta fail', uploadRetryTitle: 'Retry upload', zoomTitle: 'Vaata detaile', dragTitle: 'Liiguta / Korralda', indicatorNewTitle: 'Pole veel salvestatud', indicatorSuccessTitle: 'Uploaded', indicatorErrorTitle: 'Salvestamise viga', indicatorLoadingTitle: 'Salvestan ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/fa.js ================================================ /*! * FileInput Persian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Milad Nekofar * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['fa'] = { fileSingle: 'فایل', filePlural: 'فایل‌ها', browseLabel: 'مرور …', removeLabel: 'حذف', removeTitle: 'پاکسازی فایل‌های انتخاب شده', cancelLabel: 'لغو', cancelTitle: 'لغو بارگزاری جاری', uploadLabel: 'بارگذاری', uploadTitle: 'بارگذاری فایل‌های انتخاب شده', msgNo: 'نه', msgNoFilesSelected: 'هیچ فایلی انتخاب نشده است', msgCancelled: 'لغو شد', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'نمایش با جزییات', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'فایل "{name}" ({size} کیلوبایت) خیلی کوچک است و باید از {minSize} کیلوبایت بزرگتر باشد.', msgSizeTooLarge: 'فایل "{name}" ({size} کیلوبایت) از حداکثر مجاز {maxSize} کیلوبایت بزرگتر است.', msgFilesTooLess: 'شما باید حداقل {n} {files} فایل برای بارگذاری انتخاب کنید.', msgFilesTooMany: 'تعداد فایل‌های انتخاب شده برای بارگذاری ({n}) از حداکثر مجاز عبور کرده است {m}.', msgFileNotFound: 'فایل "{name}" یافت نشد!', msgFileSecured: 'محدودیت های امنیتی مانع خواندن فایل "{name}" است.', msgFileNotReadable: 'فایل "{name}" قابل نوشتن نیست.', msgFilePreviewAborted: 'پیش نمایش فایل "{name}". به مشکل خورد', msgFilePreviewError: 'در هنگام خواندن فایل "{name}" خطایی رخ داد.', msgInvalidFileName: 'کاراکترهای غیرمجاز و یا ناشناخته در نام فایل "{name}".', msgInvalidFileType: 'نوع فایل "{name}" معتبر نیست. فقط "{types}" پشیبانی می‌شوند.', msgInvalidFileExtension: 'پسوند فایل "{name}" معتبر نیست. فقط "{extensions}" پشتیبانی می‌شوند.', msgFileTypes: { 'image': 'عکس', 'html': 'اچ تا ام ال', 'text': 'متن', 'video': 'ویدئو', 'audio': 'صدا', 'flash': 'فلش', 'pdf': 'پی دی اف', 'object': 'دیگر' }, msgUploadAborted: 'بارگذاری فایل به مشکل خورد.', msgUploadThreshold: 'در حال پردازش...', msgUploadBegin: 'در حال شروع...', msgUploadEnd: 'انجام شد', msgUploadEmpty: 'هیچ داده معتبری برای بارگذاری موجود نیست.', msgUploadError: 'Error', msgValidationError: 'خطای اعتبار سنجی', msgLoading: 'بارگیری فایل {index} از {files} …', msgProgress: 'بارگیری فایل {index} از {files} - {name} - {percent}% تمام شد.', msgSelected: '{n} {files} انتخاب شده', msgFoldersNotAllowed: 'فقط فایل‌ها را بکشید و رها کنید! {n} پوشه نادیده گرفته شد.', msgImageWidthSmall: 'عرض فایل تصویر "{name}" باید حداقل {size} پیکسل باشد.', msgImageHeightSmall: 'ارتفاع فایل تصویر "{name}" باید حداقل {size} پیکسل باشد.', msgImageWidthLarge: 'عرض فایل تصویر "{name}" نمیتواند از {size} پیکسل بیشتر باشد.', msgImageHeightLarge: 'ارتفاع فایل تصویر "{name}" نمی‌تواند از {size} پیکسل بیشتر باشد.', msgImageResizeError: 'یافت نشد ابعاد تصویر را برای تغییر اندازه.', msgImageResizeException: 'خطا در هنگام تغییر اندازه تصویر.
    {errors}
    ', msgAjaxError: 'به نظر مشکلی در حین {operation} روی داده است. لطفا دوباره تلاش کنید!', msgAjaxProgressError: '{operation} لغو شد', ajaxOperations: { deleteThumb: 'حذف فایل', uploadThumb: 'بارگذاری فایل', uploadBatch: 'بارگذاری جمعی فایلها', uploadExtra: 'بارگذاری با کمک فُرم' }, dropZoneTitle: 'فایل‌ها را بکشید و در اینجا رها کنید …', dropZoneClickTitle: '
    (یا برای انتخاب {files} کلیک کنید)', fileActionSettings: { removeTitle: 'حذف فایل', uploadTitle: 'آپلود فایل', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'دیدن جزئیات', dragTitle: 'جابجایی / چیدمان', indicatorNewTitle: 'آپلود نشده است', indicatorSuccessTitle: 'آپلود شده', indicatorErrorTitle: 'بارگذاری خطا', indicatorLoadingTitle: 'آپلود ...' }, previewZoomButtonTitles: { prev: 'مشاهده فایل قبلی', next: 'مشاهده فایل بعدی', toggleheader: 'نمایش عنوان', fullscreen: 'نمایش تمام صفحه', borderless: 'نمایش حاشیه', close: 'بستن نمایش با جزییات' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/fi.js ================================================ /*! * FileInput Finnish Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales.fi = { fileSingle: 'tiedosto', filePlural: 'tiedostot', browseLabel: 'Selaa …', removeLabel: 'Poista', removeTitle: 'Tyhjännä valitut tiedostot', cancelLabel: 'Peruuta', cancelTitle: 'Peruuta lataus', uploadLabel: 'Lataa', uploadTitle: 'Lataa valitut tiedostot', msgNoFilesSelected: '', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Tiedosto "{name}" ({size} Kt) ylittää suurimman sallitun tiedoston koon, joka on {maxSize} Kt. Yritä uudelleen!', msgFilesTooLess: 'Vähintään {n} {files} tiedostoa on valittava ladattavaksi. Ole hyvä ja yritä uudelleen!', msgFilesTooMany: 'Valittujen tiedostojen lukumäärä ({n}) ylittää suurimman sallitun määrän {m}. Ole hyvä ja yritä uudelleen!', msgFileNotFound: 'Tiedostoa "{name}" ei löydy!', msgFileSecured: 'Tietoturvarajoitukset estävät tiedoston "{name}" lukemisen.', msgFileNotReadable: 'Tiedosto "{name}" ei ole luettavissa.', msgFilePreviewAborted: 'Tiedoston "{name}" esikatselu keskeytetty.', msgFilePreviewError: 'Virhe on tapahtunut luettaessa tiedostoa "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Tiedosto "{name}" on väärän tyyppinen. Ainoastaan tiedostot tyyppiä "{types}" ovat tuettuja.', msgInvalidFileExtension: 'Tiedoston "{name}" tarkenne on epäkelpo. Ainoastaan tarkenteet "{extensions}" ovat tuettuja.', msgFileTypes: { 'image': 'Kuva', 'html': 'HTML', 'text': 'Teksti', 'video': 'Video', 'audio': 'Ääni', 'flash': 'Flash', 'pdf': 'PDF', 'object': 'Olio' }, msgUploadThreshold: 'Käsitellään...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'Ei ladattavaa dataa.', msgUploadError: 'Error', msgValidationError: 'Tiedoston latausvirhe', msgLoading: 'Ladataan tiedostoa {index} / {files} …', msgProgress: 'Ladataan tiedostoa {index} / {files} - {name} - {percent}% valmistunut.', msgSelected: '{n} tiedostoa valittu', msgFoldersNotAllowed: 'Raahaa ja pudota ainoastaan tiedostoja! Ohitettu {n} raahattua kansiota.', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Raahaa ja pudota tiedostot tähän …', dropZoneClickTitle: '
    (tai valitse hiirellä {files})', fileActionSettings: { removeTitle: 'Poista tiedosto', uploadTitle: 'Upload file', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Yksityiskohdat', dragTitle: 'Siirrä / Järjestele', indicatorNewTitle: 'Ei ladattu', indicatorSuccessTitle: 'Ladattu', indicatorErrorTitle: 'Lataus epäonnistui', indicatorLoadingTitle: 'Ladataan ...' }, previewZoomButtonTitles: { prev: 'Seuraava tiedosto', next: 'Edellinen tiedosto', toggleheader: 'Näytä otsikko', fullscreen: 'Kokonäytön tila', borderless: 'Rajaton tila', close: 'Sulje esikatselu' } }; $.extend($.fn.fileinput.defaults, $.fn.fileinputLocales.fi); })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/fr.js ================================================ /*! * FileInput French Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['fr'] = { fileSingle: 'fichier', filePlural: 'fichiers', browseLabel: 'Parcourir…', removeLabel: 'Retirer', removeTitle: 'Retirer les fichiers sélectionnés', cancelLabel: 'Annuler', cancelTitle: "Annuler l'envoi en cours", uploadLabel: 'Transférer', uploadTitle: 'Transférer les fichiers sélectionnés', msgNo: 'Non', msgNoFilesSelected: '', msgCancelled: 'Annulé', msgPlaceholder: 'Sélectionner le(s) {files}...', msgZoomModalHeading: 'Aperçu détaillé', msgFileRequired: 'Vous devez sélectionner un fichier à uploader.', msgSizeTooSmall: 'Le fichier "{name}" ({size} KB) est inférieur à la taille minimale de {minSize} KB.', msgSizeTooLarge: 'Le fichier "{name}" ({size} Ko) dépasse la taille maximale autorisée qui est de {maxSize} Ko.', msgFilesTooLess: 'Vous devez sélectionner au moins {n} {files} à transmettre.', msgFilesTooMany: 'Le nombre de fichier sélectionné ({n}) dépasse la quantité maximale autorisée qui est de {m}.', msgFileNotFound: 'Le fichier "{name}" est introuvable !', msgFileSecured: "Des restrictions de sécurité vous empêchent d'accéder au fichier \"{name}\".", msgFileNotReadable: 'Le fichier "{name}" est illisible.', msgFilePreviewAborted: 'Prévisualisation du fichier "{name}" annulée.', msgFilePreviewError: 'Une erreur est survenue lors de la lecture du fichier "{name}".', msgInvalidFileName: 'Caractères invalides ou non supportés dans le nom de fichier "{name}".', msgInvalidFileType: 'Type de document invalide pour "{name}". Seulement les documents de type "{types}" sont autorisés.', msgInvalidFileExtension: 'Extension invalide pour le fichier "{name}". Seules les extensions "{extensions}" sont autorisées.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Le transfert du fichier a été interrompu', msgUploadThreshold: 'En cours...', msgUploadBegin: 'Initialisation...', msgUploadEnd: 'Terminé', msgUploadEmpty: 'Aucune donnée valide disponible pour transmission.', msgUploadError: 'Erreur', msgValidationError: 'Erreur de validation', msgLoading: 'Transmission du fichier {index} sur {files}…', msgProgress: 'Transmission du fichier {index} sur {files} - {name} - {percent}%.', msgSelected: '{n} {files} sélectionné(s)', msgFoldersNotAllowed: 'Glissez et déposez uniquement des fichiers ! {n} répertoire(s) exclu(s).', msgImageWidthSmall: 'La largeur de l\'image "{name}" doit être d\'au moins {size} px.', msgImageHeightSmall: 'La hauteur de l\'image "{name}" doit être d\'au moins {size} px.', msgImageWidthLarge: 'La largeur de l\'image "{name}" ne peut pas dépasser {size} px.', msgImageHeightLarge: 'La hauteur de l\'image "{name}" ne peut pas dépasser {size} px.', msgImageResizeError: "Impossible d'obtenir les dimensions de l'image à redimensionner.", msgImageResizeException: "Erreur lors du redimensionnement de l'image.
    {errors}
    ", msgAjaxError: "Une erreur s'est produite pendant l'opération de {operation}. Veuillez réessayer plus tard.", msgAjaxProgressError: 'L\'opération "{operation}" a échoué', ajaxOperations: { deleteThumb: 'suppression du fichier', uploadThumb: 'transfert du fichier', uploadBatch: 'transfert des fichiers', uploadExtra: 'soumission des données de formulaire' }, dropZoneTitle: 'Glissez et déposez les fichiers ici…', dropZoneClickTitle: '
    (ou cliquez pour sélectionner manuellement)', fileActionSettings: { removeTitle: 'Supprimer le fichier', uploadTitle: 'Transférer le fichier', uploadRetryTitle: 'Relancer le transfert', zoomTitle: 'Voir les détails', dragTitle: 'Déplacer / Réarranger', indicatorNewTitle: 'Pas encore transféré', indicatorSuccessTitle: 'Posté', indicatorErrorTitle: 'Ajouter erreur', indicatorLoadingTitle: 'En cours...' }, previewZoomButtonTitles: { prev: 'Voir le fichier précédent', next: 'Voir le fichier suivant', toggleheader: 'Masquer le titre', fullscreen: 'Mode plein écran', borderless: 'Mode cinéma', close: "Fermer l'aperçu" } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/gl.js ================================================ /*! * FileInput Galician Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['gl'] = { fileSingle: 'arquivo', filePlural: 'arquivos', browseLabel: 'Examinar …', removeLabel: 'Quitar', removeTitle: 'Quitar aquivos seleccionados', cancelLabel: 'Cancelar', cancelTitle: 'Abortar a subida en curso', uploadLabel: 'Subir arquivo', uploadTitle: 'Subir arquivos seleccionados', msgNo: 'Non', msgNoFilesSelected: 'Non hay arquivos seleccionados', msgCancelled: 'Cancelado', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Vista previa detallada', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'O arquivo "{name}" ({size} KB) é demasiado pequeño e debe ser maior de {minSize} KB.', msgSizeTooLarge: 'El arquivo "{name}" ({size} KB) excede o tamaño máximo permitido de {maxSize} KB.', msgFilesTooLess: 'Debe seleccionar al menos {n} {files} a cargar.', msgFilesTooMany: 'O número de arquivos seleccionados a cargar ({n}) excede do límite máximo permitido de {m}.', msgFileNotFound: 'Arquivo "{name}" non encontrado.', msgFileSecured: 'Non é posible acceder o arquivo "{name}" porque estará sendo usado por outra aplicación ou non teñamos permisos de lectura.', msgFileNotReadable: 'Non é posible acceder o archivo "{name}".', msgFilePreviewAborted: 'Previsualización do arquivo "{name}" cancelada.', msgFilePreviewError: 'Ocurriu un erro mentras se lía o arquivo "{name}".', msgInvalidFileName: 'Caracteres non válidos o no soportados no nome do arquivos "{name}".', msgInvalidFileType: 'Tipo de archivo no válido para "{name}". Sólo se permiten arquivos do tipo "{types}".', msgInvalidFileExtension: 'Extensión de arquivo non válido para "{name}". Só se permiten arquivos "{extensions}".', msgFileTypes: { 'image': 'imaxe', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'A carga de arquivos cancelouse', msgUploadThreshold: 'Procesando...', msgUploadBegin: 'Inicialicando...', msgUploadEnd: 'Feito', msgUploadEmpty: 'Non existen datos válidos para o envío.', msgUploadError: 'Error', msgValidationError: 'Erro de validación', msgLoading: 'Subindo arquivo {index} de {files} …', msgProgress: 'Subiendo arquivo {index} de {files} - {name} - {percent}% completado.', msgSelected: '{n} {files} seleccionado(s)', msgFoldersNotAllowed: 'Arrastra e solta únicamente arquivoa. Omitida(s) {n} carpeta(s).', msgImageWidthSmall: 'O ancho da imaxe "{name}" debe ser de al menos {size} px.', msgImageHeightSmall: 'A altura de la imaxe "{name}" debe ser de al menos {size} px.', msgImageWidthLarge: 'El ancho de la imaxe "{name}" no puede exceder de {size} px.', msgImageHeightLarge: 'La altura de la imaxe "{name}" no puede exceder de {size} px.', msgImageResizeError: 'No se pudieron obtener las dimensiones de la imaxe para cambiar el tamaño.', msgImageResizeException: 'Erro o cambiar o tamaño da imaxe.
    {errors}
    ', msgAjaxError: 'Algo foi mal ca operación {operation}. Por favor, intentao de novo mais tarde.', msgAjaxProgressError: 'A operación {operation} fallou', ajaxOperations: { deleteThumb: 'Arquivo borrado', uploadThumb: 'Arquivo subido', uploadBatch: 'Datos subidos en lote', uploadExtra: 'Datos do formulario subidos' }, dropZoneTitle: 'Arrasta e solte aquí os arquivos …', dropZoneClickTitle: '
    (ou fai clic para seleccionar {files})', fileActionSettings: { removeTitle: 'Eliminar arquivo', uploadTitle: 'Subir arquivo', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Ver detalles', dragTitle: 'Mover / Reordenar', indicatorNewTitle: 'Non subido todavía', indicatorSuccessTitle: 'Subido', indicatorErrorTitle: 'Erro o subir', indicatorLoadingTitle: 'Subiendo...' }, previewZoomButtonTitles: { prev: 'Ver arquivo anterior', next: 'Ver arquivo siguinte', toggleheader: 'Mostrar encabezado', fullscreen: 'Mostrar a pantalla completa', borderless: 'Activar o modo sen bordes', close: 'Cerrar vista detallada' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/hu.js ================================================ /*! * FileInput Hungarian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['hu'] = { fileSingle: 'fájl', filePlural: 'fájlok', browseLabel: 'Tallóz …', removeLabel: 'Eltávolít', removeTitle: 'Kijelölt fájlok törlése', cancelLabel: 'Mégse', cancelTitle: 'Feltöltés megszakítása', uploadLabel: 'Feltöltés', uploadTitle: 'Kijelölt fájlok feltöltése', msgNo: 'Nem', msgNoFilesSelected: 'Nincs fájl kiválasztva', msgCancelled: 'Megszakítva', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Részletes Előnézet', msgFileRequired: 'Kötelező fájlt kiválasztani a feltöltéshez.', msgSizeTooSmall: 'A fájl: "{name}" ({size} KB) mérete túl kicsi, nagyobbnak kell lennie, mint {minSize} KB.', msgSizeTooLarge: '"{name}" fájl ({size} KB) mérete nagyobb a megengedettnél {maxSize} KB.', msgFilesTooLess: 'Legalább {n} {files} ki kell választania a feltöltéshez.', msgFilesTooMany: 'A feltölteni kívánt fájlok száma ({n}) elérte a megengedett maximumot {m}.', msgFileNotFound: '"{name}" fájl nem található!', msgFileSecured: 'Biztonsági beállítások nem engedik olvasni a fájlt "{name}".', msgFileNotReadable: '"{name}" fájl nem olvasható.', msgFilePreviewAborted: '"{name}" fájl feltöltése megszakítva.', msgFilePreviewError: 'Hiba lépett fel a "{name}" fájl olvasása közben.', msgInvalidFileName: 'Hibás vagy nem támogatott karakterek a fájl nevében "{name}".', msgInvalidFileType: 'Nem megengedett fájl "{name}". Csak a "{types}" fájl típusok támogatottak.', msgInvalidFileExtension: 'Nem megengedett kiterjesztés / fájltípus "{name}". Csak a "{extensions}" kiterjesztés(ek) / fájltípus(ok) támogatottak.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'A fájl feltöltés megszakítva', msgUploadThreshold: 'Folyamatban...', msgUploadBegin: 'Inicializálás...', msgUploadEnd: 'Kész', msgUploadEmpty: 'Nincs érvényes adat a feltöltéshez.', msgUploadError: 'Error', msgValidationError: 'Érvényesítés hiba', msgLoading: '{index} / {files} töltése …', msgProgress: 'Feltöltés: {index} / {files} - {name} - {percent}% kész.', msgSelected: '{n} {files} kiválasztva.', msgFoldersNotAllowed: 'Csak fájlokat húzzon ide! Kihagyva {n} könyvtár.', msgImageWidthSmall: 'A kép szélességének "{name}" legalább {size} pixelnek kell lennie.', msgImageHeightSmall: 'A kép magasságának "{name}" legalább {size} pixelnek kell lennie.', msgImageWidthLarge: 'A kép szélessége "{name}" nem haladhatja meg a {size} pixelt.', msgImageHeightLarge: 'A kép magassága "{name}" nem haladhatja meg a {size} pixelt.', msgImageResizeError: 'Nem lehet megállapítani a kép méreteit az átméretezéshez.', msgImageResizeException: 'Hiba történt a méretezés közben.
    {errors}
    ', msgAjaxError: 'Hiba történt a művelet közben ({operation}). Kérjük, próbálja később!', msgAjaxProgressError: 'Hiba! ({operation})', ajaxOperations: { deleteThumb: 'fájl törlés', uploadThumb: 'fájl feltöltés', uploadBatch: 'csoportos fájl feltöltés', uploadExtra: 'űrlap adat feltöltés' }, dropZoneTitle: 'Húzzon ide fájlokat …', dropZoneClickTitle: '
    (vagy kattintson ide a {files} tallózásához...)', fileActionSettings: { removeTitle: 'A fájl eltávolítása', uploadTitle: 'fájl feltöltése', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Részletek megtekintése', dragTitle: 'Mozgatás / Átrendezés', indicatorNewTitle: 'Nem feltöltött', indicatorSuccessTitle: 'Feltöltött', indicatorErrorTitle: 'Feltöltés hiba', indicatorLoadingTitle: 'Feltöltés ...' }, previewZoomButtonTitles: { prev: 'Elöző fájl megnézése', next: 'Következő fájl megnézése', toggleheader: 'Fejléc mutatása', fullscreen: 'Teljes képernyős mód bekapcsolása', borderless: 'Keret nélküli ablak mód bekapcsolása', close: 'Részletes előnézet bezárása' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/id.js ================================================ /*! * FileInput Indonesian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Bambang Riswanto * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['id'] = { fileSingle: 'berkas', filePlural: 'berkas', browseLabel: 'Pilih File …', removeLabel: 'Hapus', removeTitle: 'Hapus berkas terpilih', cancelLabel: 'Batal', cancelTitle: 'Batalkan proses pengunggahan', uploadLabel: 'Unggah', uploadTitle: 'Unggah berkas terpilih', msgNo: 'Tidak', msgNoFilesSelected: '', msgCancelled: 'Dibatalkan', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Pratinjau terperinci', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Berkas "{name}" ({size} KB) melebihi ukuran upload maksimal yaitu {maxSize} KB.', msgFilesTooLess: 'Anda harus memilih setidaknya {n} {files} untuk diunggah.', msgFilesTooMany: '({n}) berkas yang dipilih untuk diunggah melebihi ukuran upload maksimal yaitu {m}.', msgFileNotFound: 'Berkas "{name}" tak ditemukan!', msgFileSecured: 'Sistem keamanan mencegah untuk membaca berkas "{name}".', msgFileNotReadable: 'Berkas "{name}" tak dapat dibaca.', msgFilePreviewAborted: 'Pratinjau untuk berkas "{name}" dibatalkan.', msgFilePreviewError: 'Kesalahan saat membaca berkas "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Jenis berkas "{name}" tidak sah. Hanya berkas "{types}" yang didukung.', msgInvalidFileExtension: 'Ekstensi berkas "{name}" tidak sah. Hanya ekstensi "{extensions}" yang didukung.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Pengunggahan berkas dibatalkan', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Kesalahan validasi', msgLoading: 'Memuat {index} dari {files} berkas …', msgProgress: 'Memuat {index} dari {files} berkas - {name} - {percent}% selesai.', msgSelected: '{n} {files} dipilih', msgFoldersNotAllowed: 'Hanya tahan dan lepas file saja! {n} folder diabaikan.', msgImageWidthSmall: 'Lebar dari gambar "{name}" harus sekurangnya {size} px.', msgImageHeightSmall: 'Tinggi dari gambar "{name}" harus sekurangnya {size} px.', msgImageWidthLarge: 'Lebar dari gambar "{name}" tak boleh melebihi {size} px.', msgImageHeightLarge: 'Tinggi dari gambar "{name}" tak boleh melebihi {size} px.', msgImageResizeError: 'Tak dapat menentukan dimensi gambar untuk mengubah ukuran.', msgImageResizeException: 'Kesalahan saat mengubah ukuran gambar.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Tarik dan lepaskan berkas disini …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Hapus berkas', uploadTitle: 'Unggah berkas', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Tampilkan Rincian', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Belum diunggah', indicatorSuccessTitle: 'Sudah diunggah', indicatorErrorTitle: 'Kesalahan pengunggahan', indicatorLoadingTitle: 'Mengunggah ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/it.js ================================================ /*! * FileInput Italian Translation * * Author: Lorenzo Milesi * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['it'] = { fileSingle: 'file', filePlural: 'file', browseLabel: 'Sfoglia…', removeLabel: 'Rimuovi', removeTitle: 'Rimuovi i file selezionati', cancelLabel: 'Annulla', cancelTitle: 'Annulla i caricamenti in corso', uploadLabel: 'Carica', uploadTitle: 'Carica i file selezionati', msgNo: 'No', msgNoFilesSelected: 'Nessun file selezionato', msgCancelled: 'Annullato', msgPlaceholder: 'Seleziona {files}...', msgZoomModalHeading: 'Anteprima dettagliata', msgFileRequired: 'Devi selezionare un file da caricare.', msgSizeTooSmall: 'Il file "{name}" ({size} KB) è troppo piccolo, deve essere almeno di {minSize} KB.', msgSizeTooLarge: 'Il file "{name}" ({size} KB) eccede la dimensione massima di caricamento di {maxSize} KB.', msgFilesTooLess: 'Devi selezionare almeno {n} {files} da caricare.', msgFilesTooMany: 'Il numero di file selezionati per il caricamento ({n}) eccede il numero massimo di file accettati {m}.', msgFileNotFound: 'File "{name}" non trovato!', msgFileSecured: 'Restrizioni di sicurezza impediscono la lettura del file "{name}".', msgFileNotReadable: 'Il file "{name}" non è leggibile.', msgFilePreviewAborted: 'Generazione anteprima per "{name}" annullata.', msgFilePreviewError: 'Errore durante la lettura del file "{name}".', msgInvalidFileName: 'Carattere non valido o non supportato nel file "{name}".', msgInvalidFileType: 'Tipo non valido per il file "{name}". Sono ammessi solo file di tipo "{types}".', msgInvalidFileExtension: 'Estensione non valida per il file "{name}". Sono ammessi solo file con estensione "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Il caricamento del file è stato interrotto', msgUploadThreshold: 'In lavorazione...', msgUploadBegin: 'Inizializzazione...', msgUploadEnd: 'Fatto', msgUploadEmpty: 'Dati non disponibili', msgUploadError: 'Errore', msgValidationError: 'Errore di convalida', msgLoading: 'Caricamento file {index} di {files}…', msgProgress: 'Caricamento file {index} di {files} - {name} - {percent}% completato.', msgSelected: '{n} {files} selezionati', msgFoldersNotAllowed: 'Trascina solo file! Ignorata/e {n} cartella/e.', msgImageWidthSmall: 'La larghezza dell\'immagine "{name}" deve essere di almeno {size} px.', msgImageHeightSmall: 'L\'altezza dell\'immagine "{name}" deve essere di almeno {size} px.', msgImageWidthLarge: 'La larghezza dell\'immagine "{name}" non può superare {size} px.', msgImageHeightLarge: 'L\'altezza dell\'immagine "{name}" non può superare {size} px.', msgImageResizeError: 'Impossibile ottenere le dimensioni dell\'immagine per ridimensionare.', msgImageResizeException: 'Errore durante il ridimensionamento dell\'immagine.
    {errors}
    ', msgAjaxError: 'Qualcosa non ha funzionato con l\'operazione {operation}. Per favore riprova più tardi!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'eliminazione file', uploadThumb: 'caricamento file', uploadBatch: 'caricamento file in batch', uploadExtra: 'upload dati del form' }, dropZoneTitle: 'Trascina i file qui…', dropZoneClickTitle: '
    (o clicca per selezionare {files})', fileActionSettings: { removeTitle: 'Rimuovere il file', uploadTitle: 'Caricare un file', uploadRetryTitle: 'Riprova il caricamento', downloadTitle: 'Scarica file', zoomTitle: 'Guarda i dettagli', dragTitle: 'Muovi / Riordina', indicatorNewTitle: 'Non ancora caricato', indicatorSuccessTitle: 'Caricati', indicatorErrorTitle: 'Carica Errore', indicatorLoadingTitle: 'Caricamento ...' }, previewZoomButtonTitles: { prev: 'Vedi il file precedente', next: 'Vedi il file seguente', toggleheader: 'Attiva header', fullscreen: 'Attiva full screen', borderless: 'Abilita modalità senza bordi', close: 'Chiudi' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ja.js ================================================ /*! * FileInput Japanese Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Yuta Hoshina * * NOTE: this file must be saved in UTF-8 encoding. * slugCallback * \u4e00-\u9fa5 : Kanji (Chinese characters) * \u3040-\u309f : Hiragana (Japanese syllabary) * \u30a0-\u30ff\u31f0-\u31ff : Katakana (including phonetic extension) * \u3200-\u32ff : Enclosed CJK Letters and Months * \uff00-\uffef : Halfwidth and Fullwidth Forms */ (function ($) { "use strict"; $.fn.fileinputLocales['ja'] = { fileSingle: 'ファイル', filePlural: 'ファイル', browseLabel: 'ファイルを選択…', removeLabel: '削除', removeTitle: '選択したファイルを削除', cancelLabel: 'キャンセル', cancelTitle: 'アップロードをキャンセル', uploadLabel: 'アップロード', uploadTitle: '選択したファイルをアップロード', msgNo: 'いいえ', msgNoFilesSelected: 'ファイルが選択されていません', msgCancelled: 'キャンセル', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'プレビュー', msgFileRequired: 'ファイルを選択してください', msgSizeTooSmall: 'ファイル"{name}" ({size} KB)はアップロード可能な下限容量{minSize} KBより小さいです', msgSizeTooLarge: 'ファイル"{name}" ({size} KB)はアップロード可能な上限容量{maxSize} KBを超えています', msgFilesTooLess: '最低{n}個の{files}を選択してください', msgFilesTooMany: '選択したファイルの数({n}個)はアップロード可能な上限数({m}個)を超えています', msgFileNotFound: 'ファイル"{name}"はありませんでした', msgFileSecured: 'ファイル"{name}"は読み取り権限がないため取得できません', msgFileNotReadable: 'ファイル"{name}"は読み込めません', msgFilePreviewAborted: 'ファイル"{name}"のプレビューを中止しました', msgFilePreviewError: 'ファイル"{name}"の読み込み中にエラーが発生しました', msgInvalidFileName: 'ファイル名に無効な文字が含まれています "{name}".', msgInvalidFileType: '"{name}"は無効なファイル形式です。"{types}"形式のファイルのみサポートしています', msgInvalidFileExtension: '"{name}"は無効な拡張子です。拡張子が"{extensions}"のファイルのみサポートしています', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'ファイルのアップロードが中止されました', msgUploadThreshold: '処理中...', msgUploadBegin: '初期化中...', msgUploadEnd: '完了', msgUploadEmpty: 'アップロードに有効なデータがありません', msgUploadError: 'エラー', msgValidationError: '検証エラー', msgLoading: '{files}個中{index}個目のファイルを読み込み中…', msgProgress: '{files}個中{index}個のファイルを読み込み中 - {name} - {percent}% 完了', msgSelected: '{n}個の{files}を選択', msgFoldersNotAllowed: 'ドラッグ&ドロップが可能なのはファイルのみです。{n}個のフォルダ-は無視されました', msgImageWidthSmall: '画像ファイル"{name}"の幅が小さすぎます。画像サイズの幅は少なくとも{size}px必要です', msgImageHeightSmall: '画像ファイル"{name}"の高さが小さすぎます。画像サイズの高さは少なくとも{size}px必要です', msgImageWidthLarge: '画像ファイル"{name}"の幅がアップロード可能な画像サイズ({size}px)を超えています', msgImageHeightLarge: '画像ファイル"{name}"の高さがアップロード可能な画像サイズ({size}px)を超えています', msgImageResizeError: 'リサイズ時に画像サイズが取得できませんでした', msgImageResizeException: '画像のリサイズ時にエラーが発生しました。
    {errors}
    ', msgAjaxError: '{operation}実行中にエラーが発生しました。時間をおいてもう一度お試しください。', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'ファイル削除', uploadThumb: 'ファイルアップロード', uploadBatch: '一括ファイルアップロード', uploadExtra: 'フォームデータアップロード' }, dropZoneTitle: 'ファイルをドラッグ&ドロップ…', dropZoneClickTitle: '
    (または クリックして{files}を選択 )', slugCallback: function(text) { return text ? text.split(/(\\|\/)/g).pop().replace(/[^\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\u31f0-\u31ff\u3200-\u32ff\uff00-\uffef\-.\\\/ ]+/g, '') : ''; }, fileActionSettings: { removeTitle: 'ファイルを削除', uploadTitle: 'ファイルをアップロード', uploadRetryTitle: '再アップロード', zoomTitle: 'プレビュー', dragTitle: '移動 / 再配置', indicatorNewTitle: 'まだアップロードされていません', indicatorSuccessTitle: 'アップロード済み', indicatorErrorTitle: 'アップロード失敗', indicatorLoadingTitle: 'アップロード中...' }, previewZoomButtonTitles: { prev: '前のファイルを表示', next: '次のファイルを表示', toggleheader: 'ファイル情報の表示/非表示', fullscreen: 'フルスクリーン表示の開始/終了', borderless: 'フルウィンドウ表示の開始/終了', close: 'プレビューを閉じる' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ka.js ================================================ /*! * FileInput Georgian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Avtandil Kikabidze aka LONGMAN * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['ka'] = { fileSingle: 'ფაილი', filePlural: 'ფაილები', browseLabel: 'არჩევა …', removeLabel: 'წაშლა', removeTitle: 'არჩეული ფაილების წაშლა', cancelLabel: 'გაუქმება', cancelTitle: 'მიმდინარე ატვირთვის გაუქმება', uploadLabel: 'ატვირთვა', uploadTitle: 'არჩეული ფაილების ატვირთვა', msgNo: 'არა', msgNoFilesSelected: 'ფაილები არ არის არჩეული', msgCancelled: 'გაუქმებულია', msgPlaceholder: 'აირჩიეთ {files}...', msgZoomModalHeading: 'დეტალურად ნახვა', msgFileRequired: 'ატვირთვისთვის აუცილებელია ფაილის არჩევა.', msgSizeTooSmall: 'ფაილი "{name}" ({size} KB) არის ძალიან პატარა. მისი ზომა უნდა იყოს არანაკლებ {minSize} KB.', msgSizeTooLarge: 'ფაილი "{name}" ({size} KB) აჭარბებს მაქსიმალურ დასაშვებ ზომას {maxSize} KB.', msgFilesTooLess: 'უნდა აირჩიოთ მინიმუმ {n} {file} ატვირთვისთვის.', msgFilesTooMany: 'არჩეული ფაილების რაოდენობა ({n}) აჭარბებს დასაშვებ ლიმიტს {m}.', msgFileNotFound: 'ფაილი "{name}" არ მოიძებნა!', msgFileSecured: 'უსაფრთხოებით გამოწვეული შეზღუდვები კრძალავს ფაილის "{name}" წაკითხვას.', msgFileNotReadable: 'ფაილის "{name}" წაკითხვა შეუძლებელია.', msgFilePreviewAborted: 'პრევიუ გაუქმებულია ფაილისათვის "{name}".', msgFilePreviewError: 'დაფიქსირდა შეცდომა ფაილის "{name}" კითხვისას.', msgInvalidFileName: 'ნაპოვნია დაუშვებელი სიმბოლოები ფაილის "{name}" სახელში.', msgInvalidFileType: 'ფაილს "{name}" გააჩნია დაუშვებელი ტიპი. მხოლოდ "{types}" ტიპის ფაილები არის დაშვებული.', msgInvalidFileExtension: 'ფაილს "{name}" გააჩნია დაუშვებელი გაფართოება. მხოლოდ "{extensions}" გაფართოების ფაილები არის დაშვებული.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'ფაილის ატვირთვა შეწყდა', msgUploadThreshold: 'მუშავდება...', msgUploadBegin: 'ინიციალიზაცია...', msgUploadEnd: 'დასრულებულია', msgUploadEmpty: 'ატვირთვისთვის დაუშვებელი მონაცემები.', msgUploadError: 'ატვირთვის შეცდომა', msgValidationError: 'ვალიდაციის შეცდომა', msgLoading: 'ატვირთვა {index} / {files} …', msgProgress: 'ფაილის ატვირთვა დასრულებულია {index} / {files} - {name} - {percent}%.', msgSelected: 'არჩეულია {n} {file}', msgFoldersNotAllowed: 'დაშვებულია მხოლოდ ფაილების გადმოთრევა! გამოტოვებულია {n} გადმოთრეული ფოლდერი.', msgImageWidthSmall: 'სურათის "{name}" სიგანე უნდა იყოს არანაკლებ {size} px.', msgImageHeightSmall: 'სურათის "{name}" სიმაღლე უნდა იყოს არანაკლებ {size} px.', msgImageWidthLarge: 'სურათის "{name}" სიგანე არ უნდა აღემატებოდეს {size} px-ს.', msgImageHeightLarge: 'სურათის "{name}" სიმაღლე არ უნდა აღემატებოდეს {size} px-ს.', msgImageResizeError: 'ვერ მოხერხდა სურათის ზომის შეცვლისთვის საჭირო მონაცემების გარკვევა.', msgImageResizeException: 'შეცდომა სურათის ზომის შეცვლისას.
    {errors}
    ', msgAjaxError: 'დაფიქსირდა შეცდომა ოპერაციის {operation} შესრულებისას. ცადეთ მოგვიანებით!', msgAjaxProgressError: 'ვერ მოხერხდა ოპერაციის {operation} შესრულება', ajaxOperations: { deleteThumb: 'ფაილის წაშლა', uploadThumb: 'ფაილის ატვირთვა', uploadBatch: 'ფაილების ატვირთვა', uploadExtra: 'მონაცემების გაგზავნა ფორმიდან' }, dropZoneTitle: 'გადმოათრიეთ ფაილები აქ …', dropZoneClickTitle: '
    (ან დააჭირეთ რათა აირჩიოთ {files})', fileActionSettings: { removeTitle: 'ფაილის წაშლა', uploadTitle: 'ფაილის ატვირთვა', uploadRetryTitle: 'ატვირთვის გამეორება', downloadTitle: 'ფაილის ჩამოტვირთვა', zoomTitle: 'დეტალურად ნახვა', dragTitle: 'გადაადგილება / მიმდევრობის შეცვლა', indicatorNewTitle: 'ჯერ არ ატვირთულა', indicatorSuccessTitle: 'ატვირთულია', indicatorErrorTitle: 'ატვირთვის შეცდომა', indicatorLoadingTitle: 'ატვირთვა ...' }, previewZoomButtonTitles: { prev: 'წინა ფაილის ნახვა', next: 'შემდეგი ფაილის ნახვა', toggleheader: 'სათაურის დამალვა', fullscreen: 'მთელ ეკრანზე გაშლა', borderless: 'მთელ გვერდზე გაშლა', close: 'დახურვა' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/kr.js ================================================ /*! * FileInput Korean Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['kr'] = { fileSingle: '파일', filePlural: '파일들', browseLabel: '찾기 …', removeLabel: '지우기', removeTitle: '선택한 파일들 지우기', cancelLabel: '취소', cancelTitle: '업로드 중단하기', uploadLabel: '업로드', uploadTitle: '선택한 파일 업로드하기', msgNo: '아니요', msgNoFilesSelected: '선택한 파일이 없습니다.', msgCancelled: '취소되었습니다.', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: '자세한 미리보기', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: '파일 "{name}" ({size} KB)이 너무 작습니다. {minSize} KB보다 용량이 커야 합니다..', msgSizeTooLarge: '파일 "{name}" ({size} KB)이 너무 큽니다. 허용 파일 사이즈는 {maxSize} KB.입니다.', msgFilesTooLess: '업로드하기 위해 최소 {n} {files}개의 파일을 선택해야 합니다.', msgFilesTooMany: '선택한 파일의 수 ({n})가 업로드 허용 최고치인 {m}를 넘었습니다..', msgFileNotFound: '파일 "{name}"을 찾을 수 없습니다.!', msgFileSecured: '보안상의 이유로 파일 "{name}"을/를 읽을 수 없습니다..', msgFileNotReadable: '파일 "{name}"은/는 읽을 수 없습니다.', msgFilePreviewAborted: '파일 "{name}"의 미리보기가 중단되었습니다.', msgFilePreviewError: '파일 "{name}"을/를 읽다가 에러가 발생했습니다.', msgInvalidFileName: '파일 "{name}" 중 지원 불가능한 문자가 포함되어 있습니다.', msgInvalidFileType: '파일 "{name}"의 타입은 지원하지 않습니다. "{types}" 타입의 파일을 선택해 주십시요.', msgInvalidFileExtension: '파일 "{name}"의 익스텐션은 지원하지 않습니다. "{extensions}" 타입의 익스텐션을 선택해 주십시요.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: '파일 업로드가 중단되었습니다.', msgUploadThreshold: '업로드 중...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: '업로드 가능 데이터가 존재하지 않습니다.', msgUploadError: 'Error', msgValidationError: '유효성 오류', msgLoading: '파일 {files} 중 {index}번째를 로딩하고 있습니다. …', msgProgress: '파일 {files}의 {name}이 {percent}% 로딩되었습니다. ', msgSelected: '{n} {files}이 선택 되었습니다.', msgFoldersNotAllowed: '드래그 앤 드랍 파일만 가능합니다! 드랍한 {n}번째 폴더를 건너 뛰었습니다.', msgImageWidthSmall: '이미지 파일 "{name}"의 가로는 최소 {size} px가 되어야 합니다.', msgImageHeightSmall: '이미지 파일 "{name}"의 세로는 최소 {size} px가 되어야 합니다.', msgImageWidthLarge: '이미지 파일 "{name}"의 가로는 최대 {size} px를 넘을수 없습니다.', msgImageHeightLarge: '이미지 파일 "{name}"의 세로는 최대 {size} px를 넘을수 없습니다.', msgImageResizeError: '이미지의 사이즈를 재조정을 위한 이미지 사이즈를 가져올 수 없습니다.', msgImageResizeException: '이미지 사이즈 재조정이 다음 이유로 실패했습니다.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: '파일을 여기에 드래그인 드랍을 하십시요 …', dropZoneClickTitle: '
    (또는 {files} 선택을 위해 클릭하십시요)', fileActionSettings: { removeTitle: '파일 지우기', uploadTitle: '파일 업로드 하기', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: '세부 정보 보기', dragTitle: '옭기기 / 재배열하기', indicatorNewTitle: '아직 업로드가 안되었습니다.', indicatorSuccessTitle: '업로드가 성공하였습니다.', indicatorErrorTitle: '업로드 중 에러가 발행했습니다.', indicatorLoadingTitle: '업로드 중 ...' }, previewZoomButtonTitles: { prev: '전 파일 보기', next: '다음 파일 보기', toggleheader: '머릿글 토글하기', fullscreen: '전채화면 토글하기', borderless: '무 테두리 토글하기', close: '세부 정보 미리보기 토글하기' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/kz.js ================================================ /*! * FileInput Kazakh Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Kali Toleugazy * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['kz'] = { fileSingle: 'файл', filePlural: 'файлдар', browseLabel: 'Таңдау …', removeLabel: 'Жою', removeTitle: 'Таңдалған файлдарды жою', cancelLabel: 'Күшін жою', cancelTitle: 'Ағымдағы жүктеуді болдырмау', uploadLabel: 'Жүктеу', uploadTitle: 'Таңдалған файлдарды жүктеу', msgNo: 'жоқ', msgNoFilesSelected: 'Файл таңдалмады', msgCancelled: 'Күші жойылған', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Алдын ала толық көру', msgSizeTooLarge: 'Файл "{name}" ({size} KB) ең үлкен {maxSize} KB өлшемінен асады.', msgFilesTooLess: 'Жүктеу үшіy кемінде {n} {files} таңдау керек.', msgFilesTooMany: 'Таңдалған ({n}) файлдардың саны берілген {m} саннан асып кетті.', msgFileNotFound: 'Файл "{name}" табылмады!', msgFileSecured: 'Шектеу қауіпсіздігі "{name}" файлын оқуға тыйым салады.', msgFileNotReadable: '"{name}" файлды оқу мүмкін емес.', msgFilePreviewAborted: '"{name}" файл үшін алдын ала қарап көру тыйым салынған.', msgFilePreviewError: '"{name}" файлды оқығанда қате пайда болды.', msgInvalidFileType: '"{name}" тыйым салынған файл түрі. Тек мынаналарға рұқсат етілген: "{types}"', msgInvalidFileExtension: '"{name}" тыйым салынған файл кеңейтімі. Тек "{extensions}" рұқсат.', msgUploadAborted: 'Файлды жүктеу доғарылды', msgUploadThreshold: 'Өңдеу...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Тексеру қатесі', msgLoading: '{index} файлды {files} … жүктеу', msgProgress: '{index} файлды {files} - {name} - {percent}% жүктеу аяқталды.', msgSelected: 'Таңдалған файлдар саны: {n}', msgFoldersNotAllowed: 'Тек файлдарды сүйреу рұқсат! {n} папка өткізілген.', msgImageWidthSmall: '{name} суреттің ені {size} px. аз болмау керек', msgImageHeightSmall: '{name} суреттің биіктігі {size} px. аз болмау керек', msgImageWidthLarge: '"{name}" суреттің ені {size} px. аспау керек', msgImageHeightLarge: '"{name}" суреттің биіктігі {size} px. аспау керек', msgImageResizeError: 'Суреттің өлшемін өзгерту үшін, мөлшері алынбады', msgImageResizeException: 'Суреттің мөлшерлерін өзгерткен кезде қателік пайда болды.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Файлдарды осында сүйреу …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Файлды өшіру', uploadTitle: 'Файлды жүктеу', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'мәліметтерді көру', dragTitle: 'Орнын ауыстыру', indicatorNewTitle: 'Жүктелген жоқ', indicatorSuccessTitle: 'Жүктелген', indicatorErrorTitle: 'Жүктелу қатесі ', indicatorLoadingTitle: 'Жүктелу ...' }, previewZoomButtonTitles: { prev: 'Алдыңғы файлды қарау', next: 'Келесі файлды қарау', toggleheader: 'Тақырыпты ауыстыру', fullscreen: 'Толық экран режимін қосу', borderless: 'Жиексіз режиміне ауысу', close: 'Толық көрінісін жабу' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/lt.js ================================================ /*! * FileInput <_LANG_> Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Mindaugas Varkalys * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['lt'] = { fileSingle: 'failas', filePlural: 'failai', browseLabel: 'Naršyti …', removeLabel: 'Šalinti', removeTitle: 'Pašalinti pasirinktus failus', cancelLabel: 'Atšaukti', cancelTitle: 'Atšaukti vykstantį įkėlimą', uploadLabel: 'Įkelti', uploadTitle: 'Įkelti pasirinktus failus', msgNo: 'Ne', msgNoFilesSelected: 'Nepasirinkta jokių failų', msgCancelled: 'Atšaukta', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detali Peržiūra', msgFileRequired: 'Pasirinkite failą įkėlimui.', msgSizeTooSmall: 'Failas "{name}" ({size} KB) yra per mažas ir turi būti didesnis nei {minSize} KB.', msgSizeTooLarge: 'Failas "{name}" ({size} KB) viršija maksimalų leidžiamą įkeliamo failo dydį {maxSize} KB.', msgFilesTooLess: 'Turite pasirinkti bent {n} failus įkėlimui.', msgFilesTooMany: 'Įkėlimui pasirinktų failų skaičius ({n}) viršija maksimalų leidžiamą limitą {m}.', msgFileNotFound: 'Failas "{name}" nerastas!', msgFileSecured: 'Saugumo apribojimai neleidžia perskaityti failo "{name}".', msgFileNotReadable: 'Failas "{name}" neperskaitomas.', msgFilePreviewAborted: 'Failo peržiūra nutraukta "{name}".', msgFilePreviewError: 'Įvyko klaida skaitant failą "{name}".', msgInvalidFileName: 'Klaidingi arba nepalaikomi simboliai failo pavadinime "{name}".', msgInvalidFileType: 'Klaidingas failo "{name}" tipas. Tik "{types}" tipai yra palaikomi.', msgInvalidFileExtension: 'Klaidingas failo "{name}" plėtinys. Tik "{extensions}" plėtiniai yra palaikomi.', msgFileTypes: { 'image': 'paveikslėlis', 'html': 'HTML', 'text': 'tekstas', 'video': 'vaizdo įrašas', 'audio': 'garso įrašas', 'flash': 'flash', 'pdf': 'PDF', 'object': 'objektas' }, msgUploadAborted: 'Failo įkėlimas buvo nutrauktas', msgUploadThreshold: 'Vykdoma...', msgUploadBegin: 'Inicijuojama...', msgUploadEnd: 'Baigta', msgUploadEmpty: 'Nėra teisingų duomenų įkėlimui.', msgUploadError: 'Klaida', msgValidationError: 'Validacijos Klaida', msgLoading: 'Keliamas failas {index} iš {files} …', msgProgress: 'Keliamas failas {index} iš {files} - {name} - {percent}% baigta.', msgSelected: 'Pasirinkti {n} {files}', msgFoldersNotAllowed: 'Tempkite tik failus! Praleisti {n} nutempti aplankalas(-i).', msgImageWidthSmall: 'Paveikslėlio "{name}" plotis turi būti bent {size} px.', msgImageHeightSmall: 'Paveikslėlio "{name}" aukštis turi būti bent {size} px.', msgImageWidthLarge: 'Paveikslėlio "{name}" plotis negali viršyti {size} px.', msgImageHeightLarge: 'Paveikslėlio "{name}" aukštis negali viršyti {size} px.', msgImageResizeError: 'Nepavyksta gauti paveikslėlio matmetų, kad pakeisti jo matmemis.', msgImageResizeException: 'Klaida keičiant paveikslėlio matmenis.
    {errors}
    ', msgAjaxError: 'Kažkas nutiko vykdant {operation} operaciją. Prašome pabandyti vėliau!', msgAjaxProgressError: '{operation} operacija nesėkminga', ajaxOperations: { deleteThumb: 'failo trynimo', uploadThumb: 'failo įkėlimo', uploadBatch: 'failų rinkinio įkėlimo', uploadExtra: 'formos duomenų įkėlimo' }, dropZoneTitle: 'Tempkite failus čia …', dropZoneClickTitle: '
    (arba paspauskite, kad pasirinktumėte failus)', fileActionSettings: { removeTitle: 'Šalinti failą', uploadTitle: 'Įkelti failą', uploadRetryTitle: 'Bandyti įkelti vėl', zoomTitle: 'Peržiūrėti detales', dragTitle: 'Perstumti', indicatorNewTitle: 'Dar neįkelta', indicatorSuccessTitle: 'Įkelta', indicatorErrorTitle: 'Įkėlimo Klaida', indicatorLoadingTitle: 'Įkeliama ...' }, previewZoomButtonTitles: { prev: 'Peržiūrėti ankstesnį failą', next: 'Peržiūrėti kitą failą', toggleheader: 'Perjungti viršutinę juostą', fullscreen: 'Perjungti pilno ekrano rėžimą', borderless: 'Perjungti berėmį režimą', close: 'Uždaryti detalią peržiūrą' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/nl.js ================================================ /*! * FileInput Dutch Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['nl'] = { fileSingle: 'bestand', filePlural: 'bestanden', browseLabel: 'Zoek …', removeLabel: 'Verwijder', removeTitle: 'Verwijder geselecteerde bestanden', cancelLabel: 'Annuleren', cancelTitle: 'Annuleer upload', uploadLabel: 'Upload', uploadTitle: 'Upload geselecteerde bestanden', msgNo: 'Nee', msgNoFilesSelected: '', msgCancelled: 'Geannuleerd', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Gedetailleerd voorbeeld', msgFileRequired: 'U moet een bestand kiezen om te uploaden.', msgSizeTooSmall: 'Bestand "{name}" ({size} KB) is te klein en moet groter zijn dan {minSize} KB.', msgSizeTooLarge: 'Bestand "{name}" ({size} KB) is groter dan de toegestane {maxSize} KB.', msgFilesTooLess: 'U moet minstens {n} {files} selecteren om te uploaden.', msgFilesTooMany: 'Aantal geselecteerde bestanden ({n}) is meer dan de toegestane {m}.', msgFileNotFound: 'Bestand "{name}" niet gevonden!', msgFileSecured: 'Bestand kan niet gelezen worden in verband met beveiligings redenen "{name}".', msgFileNotReadable: 'Bestand "{name}" is niet leesbaar.', msgFilePreviewAborted: 'Bestand weergaven geannuleerd voor "{name}".', msgFilePreviewError: 'Er is een fout opgetreden met het lezen van "{name}".', msgInvalidFileName: 'Ongeldige of niet ondersteunde karakters in bestandsnaam "{name}".', msgInvalidFileType: 'Geen geldig bestand "{name}". Alleen "{types}" zijn toegestaan.', msgInvalidFileExtension: 'Geen geldige extensie "{name}". Alleen "{extensions}" zijn toegestaan.', msgFileTypes: { 'image': 'afbeelding', 'html': 'HTML', 'text': 'tekst', 'video': 'video', 'audio': 'geluid', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Het uploaden van bestanden is afgebroken', msgUploadThreshold: 'Verwerken...', msgUploadBegin: 'Initialiseren...', msgUploadEnd: 'Gedaan', msgUploadEmpty: 'Geen geldige data beschikbaar voor upload.', msgUploadError: 'Error', msgValidationError: 'Bevestiging fout', msgLoading: 'Bestanden laden {index} van de {files} …', msgProgress: 'Bestanden laden {index} van de {files} - {name} - {percent}% compleet.', msgSelected: '{n} {files} geselecteerd', msgFoldersNotAllowed: 'Drag & drop alleen bestanden! {n} overgeslagen map(pen).', msgImageWidthSmall: 'Breedte van het foto-bestand "{name}" moet minstens {size} px zijn.', msgImageHeightSmall: 'Hoogte van het foto-bestand "{name}" moet minstens {size} px zijn.', msgImageWidthLarge: 'Breedte van het foto-bestand "{name}" kan niet hoger zijn dan {size} px.', msgImageHeightLarge: 'Hoogte van het foto bestand "{name}" kan niet hoger zijn dan {size} px.', msgImageResizeError: 'Kon de foto afmetingen niet lezen om te verkleinen.', msgImageResizeException: 'Fout bij het verkleinen van de foto.
    {errors}
    ', msgAjaxError: 'Er ging iets mis met de {operation} actie. Gelieve later opnieuw te proberen!', msgAjaxProgressError: '{operation} mislukt', ajaxOperations: { deleteThumb: 'bestand verwijderen', uploadThumb: 'bestand uploaden', uploadBatch: 'alle bestanden uploaden', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & drop bestanden hier …', dropZoneClickTitle: '
    (of klik hier om {files} te selecteren)', fileActionSettings: { removeTitle: 'Verwijder bestand', uploadTitle: 'bestand uploaden', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Bekijk details', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Nog niet geupload', indicatorSuccessTitle: 'geupload', indicatorErrorTitle: 'fout uploaden', indicatorLoadingTitle: 'uploaden ...' }, previewZoomButtonTitles: { prev: 'Toon vorig bestand', next: 'Toon volgend bestand', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/no.js ================================================ /*! * FileInput Norwegian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['no'] = { fileSingle: 'fil', filePlural: 'filer', browseLabel: 'Bla gjennom …', removeLabel: 'Fjern', removeTitle: 'Fjern valgte filer', cancelLabel: 'Avbryt', cancelTitle: 'Stopp pågående opplastninger', uploadLabel: 'Last opp', uploadTitle: 'Last opp valgte filer', msgNo: 'Nei', msgNoFilesSelected: 'Ingen filer er valgt', msgCancelled: 'Avbrutt', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detaljert visning', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'Filen "{name}" ({size} KB) er for liten og må være større enn {minSize} KB.', msgSizeTooLarge: 'Filen "{name}" ({size} KB) er for stor, maksimal filstørrelse er {maxSize} KB.', msgFilesTooLess: 'Du må velge minst {n} {files} for opplastning.', msgFilesTooMany: 'For mange filer til opplastning, ({n}) overstiger maksantallet som er {m}.', msgFileNotFound: 'Fant ikke filen "{name}"!', msgFileSecured: 'Sikkerhetsrestriksjoner hindrer lesing av filen "{name}".', msgFileNotReadable: 'Filen "{name}" er ikke lesbar.', msgFilePreviewAborted: 'Filvisning avbrutt for "{name}".', msgFilePreviewError: 'En feil oppstod under lesing av filen "{name}".', msgInvalidFileName: 'Ugyldige tegn i filen "{name}".', msgInvalidFileType: 'Ugyldig type for filen "{name}". Kun "{types}" filer er tillatt.', msgInvalidFileExtension: 'Ugyldig endelse for filen "{name}". Kun "{extensions}" filer støttes.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Filopplastningen ble avbrutt', msgUploadThreshold: 'Prosesserer...', msgUploadBegin: 'Initialiserer...', msgUploadEnd: 'Ferdig', msgUploadEmpty: 'Ingen gyldige data tilgjengelig for opplastning.', msgUploadError: 'Error', msgValidationError: 'Valideringsfeil', msgLoading: 'Laster fil {index} av {files} …', msgProgress: 'Laster fil {index} av {files} - {name} - {percent}% fullført.', msgSelected: '{n} {files} valgt', msgFoldersNotAllowed: 'Kun Dra & slipp filer! Hoppet over {n} mappe(r).', msgImageWidthSmall: 'Bredde på bildefilen "{name}" må være minst {size} px.', msgImageHeightSmall: 'Høyde på bildefilen "{name}" må være minst {size} px.', msgImageWidthLarge: 'Bredde på bildefilen "{name}" kan ikke overstige {size} px.', msgImageHeightLarge: 'Høyde på bildefilen "{name}" kan ikke overstige {size} px.', msgImageResizeError: 'Fant ikke dimensjonene som skulle resizes.', msgImageResizeException: 'En feil oppstod under endring av størrelse .
    {errors}
    ', msgAjaxError: 'Noe gikk galt med {operation} operasjonen. Vennligst prøv igjen senere!', msgAjaxProgressError: '{operation} feilet', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Dra & slipp filer her …', dropZoneClickTitle: '
    (eller klikk for å velge {files})', fileActionSettings: { removeTitle: 'Fjern fil', uploadTitle: 'Last opp fil', uploadRetryTitle: 'Retry upload', zoomTitle: 'Vis detaljer', dragTitle: 'Flytt / endre rekkefølge', indicatorNewTitle: 'Opplastning ikke fullført', indicatorSuccessTitle: 'Opplastet', indicatorErrorTitle: 'Opplastningsfeil', indicatorLoadingTitle: 'Laster opp ...' }, previewZoomButtonTitles: { prev: 'Vis forrige fil', next: 'Vis neste fil', toggleheader: 'Vis header', fullscreen: 'Åpne fullskjerm', borderless: 'Åpne uten kanter', close: 'Lukk detaljer' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/pl.js ================================================ /*! * FileInput Polish Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['pl'] = { fileSingle: 'plik', filePlural: 'pliki', browseLabel: 'Przeglądaj …', removeLabel: 'Usuń', removeTitle: 'Usuń zaznaczone pliki', cancelLabel: 'Przerwij', cancelTitle: 'Anuluj wysyłanie', uploadLabel: 'Wgraj', uploadTitle: 'Wgraj zaznaczone pliki', msgNo: 'Nie', msgNoFilesSelected: 'Brak zaznaczonych plików', msgCancelled: 'Odwołany', msgPlaceholder: 'Wybierz {files}...', msgZoomModalHeading: 'Szczegółowy podgląd', msgFileRequired: 'Musisz wybrać plik do wgrania.', msgSizeTooSmall: 'Plik "{name}" ({size} KB) jest zbyt mały i musi być większy niż {minSize} KB.', msgSizeTooLarge: 'Plik o nazwie "{name}" ({size} KB) przekroczył maksymalną dopuszczalną wielkość pliku wynoszącą {maxSize} KB.', msgFilesTooLess: 'Minimalna liczba plików do wgrania: {n}.', msgFilesTooMany: 'Liczba plików wybranych do wgrania w liczbie ({n}), przekracza maksymalny dozwolony limit wynoszący {m}.', msgFileNotFound: 'Plik "{name}" nie istnieje!', msgFileSecured: 'Ustawienia zabezpieczeń uniemożliwiają odczyt pliku "{name}".', msgFileNotReadable: 'Plik "{name}" nie jest plikiem do odczytu.', msgFilePreviewAborted: 'Podgląd pliku "{name}" został przerwany.', msgFilePreviewError: 'Wystąpił błąd w czasie odczytu pliku "{name}".', msgInvalidFileName: 'Nieprawidłowe lub nieobsługiwane znaki w nazwie pliku "{name}".', msgInvalidFileType: 'Nieznany typ pliku "{name}". Tylko następujące rodzaje plików są dozwolone: "{types}".', msgInvalidFileExtension: 'Złe rozszerzenie dla pliku "{name}". Tylko następujące rozszerzenia plików są dozwolone: "{extensions}".', msgUploadAborted: 'Przesyłanie pliku zostało przerwane', msgUploadThreshold: 'Przetwarzanie...', msgUploadBegin: 'Rozpoczynanie...', msgUploadEnd: 'Gotowe!', msgUploadEmpty: 'Brak poprawnych danych do przesłania.', msgUploadError: 'Błąd', msgValidationError: 'Błąd walidacji', msgLoading: 'Wczytywanie pliku {index} z {files} …', msgProgress: 'Wczytywanie pliku {index} z {files} - {name} - {percent}% zakończone.', msgSelected: '{n} Plików zaznaczonych', msgFoldersNotAllowed: 'Metodą przeciągnij i upuść, można przenosić tylko pliki. Pominięto {n} katalogów.', msgImageWidthSmall: 'Szerokość pliku obrazu "{name}" musi być co najmniej {size} px.', msgImageHeightSmall: 'Wysokość pliku obrazu "{name}" musi być co najmniej {size} px.', msgImageWidthLarge: 'Szerokość pliku obrazu "{name}" nie może przekraczać {size} px.', msgImageHeightLarge: 'Wysokość pliku obrazu "{name}" nie może przekraczać {size} px.', msgImageResizeError: 'Nie udało się uzyskać wymiaru obrazu, aby zmienić rozmiar.', msgImageResizeException: 'Błąd podczas zmiany rozmiaru obrazu.
    {errors}
    ', msgAjaxError: 'Coś poczło nie tak podczas {operation}. Spróbuj ponownie!', msgAjaxProgressError: '{operation} nie powiodło się', ajaxOperations: { deleteThumb: 'usuwanie pliku', uploadThumb: 'przesyłanie pliku', uploadBatch: 'masowe przesyłanie plików', uploadExtra: 'przesyłanie danych formularza' }, dropZoneTitle: 'Przeciągnij i upuść pliki tutaj …', dropZoneClickTitle: '
    (lub kliknij tutaj i wybierz {files} z komputera)', fileActionSettings: { removeTitle: 'Usuń plik', uploadTitle: 'Przesyłanie pliku', uploadRetryTitle: 'Ponów', downloadTitle: 'Pobierz plik', zoomTitle: 'Pokaż szczegóły', dragTitle: 'Przenies / Ponownie zaaranżuj', indicatorNewTitle: 'Jeszcze nie przesłany', indicatorSuccessTitle: 'Dodane', indicatorErrorTitle: 'Błąd', indicatorLoadingTitle: 'Przesyłanie ...' }, previewZoomButtonTitles: { prev: 'Pokaż poprzedni plik', next: 'Pokaż następny plik', toggleheader: 'Włącz / wyłącz nagłówek', fullscreen: 'Włącz / wyłącz pełny ekran', borderless: 'Włącz / wyłącz tryb bez ramek', close: 'Zamknij szczegółowy widok' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/pt-BR.js ================================================ /*! * FileInput Brazillian Portuguese Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['pt-BR'] = { fileSingle: 'arquivo', filePlural: 'arquivos', browseLabel: 'Procurar…', removeLabel: 'Remover', removeTitle: 'Remover arquivos selecionados', cancelLabel: 'Cancelar', cancelTitle: 'Interromper envio em andamento', uploadLabel: 'Enviar', uploadTitle: 'Enviar arquivos selecionados', msgNo: 'Não', msgNoFilesSelected: 'Nenhum arquivo selecionado', msgCancelled: 'Cancelado', msgPlaceholder: 'Selecionar {files}...', msgZoomModalHeading: 'Pré-visualização detalhada', msgFileRequired: 'Você deve selecionar um arquivo para enviar.', msgSizeTooSmall: 'O arquivo "{name}" ({size} KB) é muito pequeno e deve ser maior que {minSize} KB.', msgSizeTooLarge: 'O arquivo "{name}" ({size} KB) excede o tamanho máximo permitido de {maxSize} KB.', msgFilesTooLess: 'Você deve selecionar pelo menos {n} {files} para enviar.', msgFilesTooMany: 'O número de arquivos selecionados para o envio ({n}) excede o limite máximo permitido de {m}.', msgFileNotFound: 'O arquivo "{name}" não foi encontrado!', msgFileSecured: 'Restrições de segurança impedem a leitura do arquivo "{name}".', msgFileNotReadable: 'O arquivo "{name}" não pode ser lido.', msgFilePreviewAborted: 'A pré-visualização do arquivo "{name}" foi interrompida.', msgFilePreviewError: 'Ocorreu um erro ao ler o arquivo "{name}".', msgInvalidFileName: 'Caracteres inválidos ou não suportados no arquivo "{name}".', msgInvalidFileType: 'Tipo inválido para o arquivo "{name}". Apenas arquivos "{types}" são permitidos.', msgInvalidFileExtension: 'Extensão inválida para o arquivo "{name}". Apenas arquivos "{extensions}" são permitidos.', msgFileTypes: { 'image': 'imagem', 'html': 'HTML', 'text': 'texto', 'video': 'vídeo', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'objeto' }, msgUploadAborted: 'O envio do arquivo foi abortado', msgUploadThreshold: 'Processando...', msgUploadBegin: 'Inicializando...', msgUploadEnd: 'Concluído', msgUploadEmpty: 'Nenhuma informação válida para upload.', msgUploadError: 'Erro de Upload', msgValidationError: 'Erro de validação', msgLoading: 'Enviando arquivo {index} de {files}…', msgProgress: 'Enviando arquivo {index} de {files} - {name} - {percent}% completo.', msgSelected: '{n} {files} selecionado(s)', msgFoldersNotAllowed: 'Arraste e solte apenas arquivos! {n} pasta(s) ignoradas.', msgImageWidthSmall: 'Largura do arquivo de imagem "{name}" deve ser pelo menos {size} px.', msgImageHeightSmall: 'Altura do arquivo de imagem "{name}" deve ser pelo menos {size} px.', msgImageWidthLarge: 'Largura do arquivo de imagem "{name}" não pode exceder {size} px.', msgImageHeightLarge: 'Altura do arquivo de imagem "{name}" não pode exceder {size} px.', msgImageResizeError: 'Não foi possível obter as dimensões da imagem para redimensionar.', msgImageResizeException: 'Erro ao redimensionar a imagem.
    {errors}
    ', msgAjaxError: 'Algo deu errado com a operação {operation}. Por favor tente novamente mais tarde!', msgAjaxProgressError: '{operation} falhou', ajaxOperations: { deleteThumb: 'Exclusão de arquivo', uploadThumb: 'Upload de arquivos', uploadBatch: 'Carregamento de arquivos em lote', uploadExtra: 'Carregamento de dados do formulário' }, dropZoneTitle: 'Arraste e solte os arquivos aqui…', dropZoneClickTitle: '
    (ou clique para selecionar o(s) arquivo(s))', fileActionSettings: { removeTitle: 'Remover arquivo', uploadTitle: 'Enviar arquivo', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Ver detalhes', dragTitle: 'Mover / Reordenar', indicatorNewTitle: 'Ainda não enviado', indicatorSuccessTitle: 'Enviado', indicatorErrorTitle: 'Erro', indicatorLoadingTitle: 'Enviando...' }, previewZoomButtonTitles: { prev: 'Visualizar arquivo anterior', next: 'Visualizar próximo arquivo', toggleheader: 'Mostrar cabeçalho', fullscreen: 'Ativar tela cheia', borderless: 'Ativar modo sem borda', close: 'Fechar pré-visualização detalhada' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/pt.js ================================================ /*! * FileInput Portuguese Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['pt'] = { fileSingle: 'ficheiro', filePlural: 'ficheiros', browseLabel: 'Procurar …', removeLabel: 'Remover', removeTitle: 'Remover ficheiros seleccionados', cancelLabel: 'Cancelar', cancelTitle: 'Abortar carregamento ', uploadLabel: 'Carregar', uploadTitle: 'Carregar ficheiros seleccionados', msgNo: 'Não', msgNoFilesSelected: '', msgCancelled: 'Cancelado', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Pré-visualização detalhada', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Ficheiro "{name}" ({size} KB) excede o tamanho máximo permido de {maxSize} KB.', msgFilesTooLess: 'Deve seleccionar pelo menos {n} {files} para fazer upload.', msgFilesTooMany: 'Número máximo de ficheiros seleccionados ({n}) excede o limite máximo de {m}.', msgFileNotFound: 'Ficheiro "{name}" não encontrado!', msgFileSecured: 'Restrições de segurança preventem a leitura do ficheiro "{name}".', msgFileNotReadable: 'Ficheiro "{name}" não pode ser lido.', msgFilePreviewAborted: 'Pré-visualização abortado para o ficheiro "{name}".', msgFilePreviewError: 'Ocorreu um erro ao ler o ficheiro "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Tipo inválido para o ficheiro "{name}". Apenas ficheiros "{types}" são suportados.', msgInvalidFileExtension: 'Extensão inválida para o ficheiro "{name}". Apenas ficheiros "{extensions}" são suportados.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'O upload do arquivo foi abortada', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Erro de validação', msgLoading: 'A carregar ficheiro {index} de {files} …', msgProgress: 'A carregar ficheiro {index} de {files} - {name} - {percent}% completo.', msgSelected: '{n} {files} seleccionados', msgFoldersNotAllowed: 'Arrastar e largar ficheiros apenas! {n} pasta(s) ignoradas.', msgImageWidthSmall: 'Largura do arquivo de imagem "{name}" deve ser pelo menos {size} px.', msgImageHeightSmall: 'Altura do arquivo de imagem "{name}" deve ser pelo menos {size} px.', msgImageWidthLarge: 'Largura do arquivo de imagem "{name}" não pode exceder {size} px.', msgImageHeightLarge: 'Altura do arquivo de imagem "{name}" não pode exceder {size} px.', msgImageResizeError: 'Could not get the image dimensions to resize.', msgImageResizeException: 'Erro ao redimensionar a imagem.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Arrastar e largar ficheiros aqui …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Remover arquivo', uploadTitle: 'Carregar arquivo', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Ver detalhes', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Ainda não carregou', indicatorSuccessTitle: 'Carregado', indicatorErrorTitle: 'Carregar Erro', indicatorLoadingTitle: 'A carregar ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ro.js ================================================ /*! * FileInput Romanian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author Ciprian Voicu * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['ro'] = { fileSingle: 'fișier', filePlural: 'fișiere', browseLabel: 'Răsfoiește …', removeLabel: 'Șterge', removeTitle: 'Curăță fișierele selectate', cancelLabel: 'Renunță', cancelTitle: 'Anulează încărcarea curentă', uploadLabel: 'Încarcă', uploadTitle: 'Încarcă fișierele selectate', msgNo: 'Nu', msgNoFilesSelected: '', msgCancelled: 'Anulat', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Previzualizare detaliată', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Fișierul "{name}" ({size} KB) depășește limita maximă de încărcare de {maxSize} KB.', msgFilesTooLess: 'Trebuie să selectezi cel puțin {n} {files} pentru a încărca.', msgFilesTooMany: 'Numărul fișierelor pentru încărcare ({n}) depășește limita maximă de {m}.', msgFileNotFound: 'Fișierul "{name}" nu a fost găsit!', msgFileSecured: 'Restricții de securitate previn citirea fișierului "{name}".', msgFileNotReadable: 'Fișierul "{name}" nu se poate citi.', msgFilePreviewAborted: 'Fișierului "{name}" nu poate fi previzualizat.', msgFilePreviewError: 'A intervenit o eroare în încercarea de citire a fișierului "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Tip de fișier incorect pentru "{name}". Sunt suportate doar fișiere de tipurile "{types}".', msgInvalidFileExtension: 'Extensie incorectă pentru "{name}". Sunt suportate doar extensiile "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Fișierul Încărcarea a fost întrerupt', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Eroare de validare', msgLoading: 'Se încarcă fișierul {index} din {files} …', msgProgress: 'Se încarcă fișierul {index} din {files} - {name} - {percent}% încărcat.', msgSelected: '{n} {files} încărcate', msgFoldersNotAllowed: 'Se poate doar trăgând fișierele! Se renunță la {n} dosar(e).', msgImageWidthSmall: 'Lățimea de fișier de imagine "{name}" trebuie să fie de cel puțin {size} px.', msgImageHeightSmall: 'Înălțimea fișier imagine "{name}" trebuie să fie de cel puțin {size} px.', msgImageWidthLarge: 'Lățimea de fișier de imagine "{name}" nu poate depăși {size} px.', msgImageHeightLarge: 'Înălțimea fișier imagine "{name}" nu poate depăși {size} px.', msgImageResizeError: 'Nu a putut obține dimensiunile imaginii pentru a redimensiona.', msgImageResizeException: 'Eroare la redimensionarea imaginii.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Trage fișierele aici …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Scoateți fișier', uploadTitle: 'Incarca fisier', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Vezi detalii', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Nu a încărcat încă', indicatorSuccessTitle: 'încărcat', indicatorErrorTitle: 'Încărcați eroare', indicatorLoadingTitle: 'Se încarcă ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/ru.js ================================================ /*! * FileInput Russian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author CyanoFresh * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['ru'] = { fileSingle: 'файл', filePlural: 'файлы', browseLabel: 'Выбрать …', removeLabel: 'Удалить', removeTitle: 'Очистить выбранные файлы', cancelLabel: 'Отмена', cancelTitle: 'Отменить текущую загрузку', uploadLabel: 'Загрузить', uploadTitle: 'Загрузить выбранные файлы', msgNo: 'нет', msgNoFilesSelected: '', msgCancelled: 'Отменено', msgPlaceholder: 'Выбрать {files}...', msgZoomModalHeading: 'Подробное превью', msgFileRequired: 'Необходимо выбрать файл для загрузки.', msgSizeTooSmall: 'Файл "{name}" ({size} KB) имеет слишком маленький размер и должен быть больше {minSize} KB.', msgSizeTooLarge: 'Файл "{name}" ({size} KB) превышает максимальный размер {maxSize} KB.', msgFilesTooLess: 'Вы должны выбрать как минимум {n} {files} для загрузки.', msgFilesTooMany: 'Количество выбранных файлов ({n}) превышает максимально допустимое количество {m}.', msgFileNotFound: 'Файл "{name}" не найден!', msgFileSecured: 'Ограничения безопасности запрещают читать файл "{name}".', msgFileNotReadable: 'Файл "{name}" невозможно прочитать.', msgFilePreviewAborted: 'Предпросмотр отменен для файла "{name}".', msgFilePreviewError: 'Произошла ошибка при чтении файла "{name}".', msgInvalidFileName: 'Неверные или неподдерживаемые символы в названии файла "{name}".', msgInvalidFileType: 'Запрещенный тип файла для "{name}". Только "{types}" разрешены.', msgInvalidFileExtension: 'Запрещенное расширение для файла "{name}". Только "{extensions}" разрешены.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Выгрузка файла прервана', msgUploadThreshold: 'Обработка...', msgUploadBegin: 'Инициализация...', msgUploadEnd: 'Готово', msgUploadEmpty: 'Недопустимые данные для загрузки', msgUploadError: 'Ошибка загрузки', msgValidationError: 'Ошибка проверки', msgLoading: 'Загрузка файла {index} из {files} …', msgProgress: 'Загрузка файла {index} из {files} - {name} - {percent}% завершено.', msgSelected: 'Выбрано файлов: {n}', msgFoldersNotAllowed: 'Разрешено перетаскивание только файлов! Пропущено {n} папок.', msgImageWidthSmall: 'Ширина изображения {name} должна быть не меньше {size} px.', msgImageHeightSmall: 'Высота изображения {name} должна быть не меньше {size} px.', msgImageWidthLarge: 'Ширина изображения "{name}" не может превышать {size} px.', msgImageHeightLarge: 'Высота изображения "{name}" не может превышать {size} px.', msgImageResizeError: 'Не удалось получить размеры изображения, чтобы изменить размер.', msgImageResizeException: 'Ошибка при изменении размера изображения.
    {errors}
    ', msgAjaxError: 'Произошла ошибка при выполнении операции {operation}. Повторите попытку позже!', msgAjaxProgressError: 'Не удалось выполнить {operation}', ajaxOperations: { deleteThumb: 'удалить файл', uploadThumb: 'загрузить файл', uploadBatch: 'загрузить пакет файлов', uploadExtra: 'загрузка данных с формы' }, dropZoneTitle: 'Перетащите файлы сюда …', dropZoneClickTitle: '
    (Или щёлкните, чтобы выбрать {files})', fileActionSettings: { removeTitle: 'Удалить файл', uploadTitle: 'Загрузить файл', uploadRetryTitle: 'Повторить загрузку', downloadTitle: 'Загрузить файл', zoomTitle: 'Посмотреть детали', dragTitle: 'Переместить / Изменить порядок', indicatorNewTitle: 'Еще не загружен', indicatorSuccessTitle: 'Загружен', indicatorErrorTitle: 'Ошибка загрузки', indicatorLoadingTitle: 'Загрузка ...' }, previewZoomButtonTitles: { prev: 'Посмотреть предыдущий файл', next: 'Посмотреть следующий файл', toggleheader: 'Переключить заголовок', fullscreen: 'Переключить полноэкранный режим', borderless: 'Переключить режим без полей', close: 'Закрыть подробный предпросмотр' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/sk.js ================================================ /*! * FileInput Slovakian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['sk'] = { fileSingle: 'súbor', filePlural: 'súbory', browseLabel: 'Vybrať …', removeLabel: 'Odstrániť', removeTitle: 'Vyčistiť vybraté súbory', cancelLabel: 'Storno', cancelTitle: 'Prerušiť nahrávanie', uploadLabel: 'Nahrať', uploadTitle: 'Nahrať vybraté súbory', msgNo: 'Nie', msgNoFilesSelected: '', msgCancelled: 'Zrušené', msgPlaceholder: 'Vybrať {files}...', msgZoomModalHeading: 'Detailný náhľad', msgFileRequired: 'Musíte vybrať súbor, ktorý chcete nahrať.', msgSizeTooSmall: 'Súbor "{name}" ({size} KB) je príliš malý, musí mať veľkosť najmenej {minSize} KB.', msgSizeTooLarge: 'Súbor "{name}" ({size} KB) je príliš veľký, maximálna povolená veľkosť {maxSize} KB.', msgFilesTooLess: 'Musíte vybrať najmenej {n} {files} pre nahranie.', msgFilesTooMany: 'Počet vybratých súborov ({n}) prekročil maximálny povolený limit {m}.', msgFileNotFound: 'Súbor "{name}" nebol nájdený!', msgFileSecured: 'Zabezpečenie súboru znemožnilo čítať súbor "{name}".', msgFileNotReadable: 'Súbor "{name}" nie je čitateľný.', msgFilePreviewAborted: 'Náhľad súboru bol prerušený pre "{name}".', msgFilePreviewError: 'Nastala chyba pri načítaní súboru "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Neplatný typ súboru "{name}". Iba "{types}" súborov sú podporované.', msgInvalidFileExtension: 'Neplatná extenzia súboru "{name}". Iba "{extensions}" súborov sú podporované.', msgFileTypes: { 'image': 'obrázok', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Nahrávanie súboru bolo prerušené', msgUploadThreshold: 'Spracovávam...', msgUploadBegin: 'Inicializujem...', msgUploadEnd: 'Hotovo', msgUploadEmpty: 'Na nahrávanie nie sú k dispozícii žiadne platné údaje.', msgUploadError: 'Chyba', msgValidationError: 'Chyba overenia', msgLoading: 'Nahrávanie súboru {index} z {files} …', msgProgress: 'Nahrávanie súboru {index} z {files} - {name} - {percent}% dokončené.', msgSelected: '{n} {files} vybraté', msgFoldersNotAllowed: 'Tiahni a pusť iba súbory! Vynechané {n} pustené prečinok(y).', msgImageWidthSmall: 'Šírka obrázku "{name}", musí byť minimálne {size} px.', msgImageHeightSmall: 'Výška obrázku "{name}", musí byť minimálne {size} px.', msgImageWidthLarge: 'Šírka obrázku "{name}" nemôže presiahnuť {size} px.', msgImageHeightLarge: 'Výška obrázku "{name}" nesmie presiahnuť {size} px.', msgImageResizeError: 'Nepodarilo sa získať veľkosť obrázka pre zmenu veľkosti.', msgImageResizeException: 'Chyba pri zmene veľkosti obrázka.
    {errors}
    ', msgAjaxError: 'Pri operácii {operation} sa vyskytla chyba. Skúste to prosím neskôr!', msgAjaxProgressError: '{operation} - neúspešné', ajaxOperations: { deleteThumb: 'odstrániť súbor', uploadThumb: 'nahrať súbor', uploadBatch: 'nahrať várku súborov', uploadExtra: 'odosielanie údajov z formulára' }, dropZoneTitle: 'Tiahni a pusť súbory tu …', dropZoneClickTitle: '
    (alebo kliknite sem a vyberte {files})', fileActionSettings: { removeTitle: 'Odstrániť súbor', uploadTitle: 'Nahrať súbor', uploadRetryTitle: 'Znova nahrať', downloadTitle: 'Stiahnuť súbor', zoomTitle: 'Zobraziť podrobnosti', dragTitle: 'Posunúť / Preskládať', indicatorNewTitle: 'Ešte nenahral', indicatorSuccessTitle: 'Nahraný', indicatorErrorTitle: 'Chyba pri nahrávaní', indicatorLoadingTitle: 'Nahrávanie ...' }, previewZoomButtonTitles: { prev: 'Zobraziť predchádzajúci súbor', next: 'Zobraziť následujúci súbor', toggleheader: 'Prepnúť záhlavie', fullscreen: 'Prepnúť zobrazenie na celú obrazovku', borderless: 'Prepnúť na bezrámikové zobrazenie', close: 'Zatvoriť detailný náhľad' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/sl.js ================================================ /*! * FileInput Slovenian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author kv1dr * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['sl'] = { fileSingle: 'datoteka', filePlural: 'datotek', browseLabel: 'Prebrskaj …', removeLabel: 'Odstrani', removeTitle: 'Počisti izbrane datoteke', cancelLabel: 'Prekliči', cancelTitle: 'Prekliči nalaganje', uploadLabel: 'Naloži', uploadTitle: 'Naloži izbrane datoteke', msgNo: 'Ne', msgNoFilesSelected: 'Nobena datoteka ni izbrana', msgCancelled: 'Preklicano', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Podroben predogled', msgSizeTooLarge: 'Datoteka "{name}" ({size} KB) presega največjo dovoljeno velikost za nalaganje {maxSize} KB.', msgFilesTooLess: 'Za nalaganje morate izbrati vsaj {n} {files}.', msgFilesTooMany: 'Število datotek, izbranih za nalaganje ({n}) je prekoračilo največjo dovoljeno število {m}.', msgFileNotFound: 'Datoteka "{name}" ni bila najdena!', msgFileSecured: 'Zaradi varnostnih omejitev nisem mogel prebrati datoteko "{name}".', msgFileNotReadable: 'Datoteka "{name}" ni berljiva.', msgFilePreviewAborted: 'Predogled datoteke "{name}" preklican.', msgFilePreviewError: 'Pri branju datoteke "{name}" je prišlo do napake.', msgInvalidFileType: 'Napačen tip datoteke "{name}". Samo "{types}" datoteke so podprte.', msgInvalidFileExtension: 'Napačna končnica datoteke "{name}". Samo "{extensions}" datoteke so podprte.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Nalaganje datoteke je bilo preklicano', msgUploadThreshold: 'Procesiram...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Napaki pri validiranju', msgLoading: 'Nalaganje datoteke {index} od {files} …', msgProgress: 'Nalaganje datoteke {index} od {files} - {name} - {percent}% dokončano.', msgSelected: '{n} {files} izbrano', msgFoldersNotAllowed: 'Povlecite in spustite samo datoteke! Izpuščenih je bilo {n} map.', msgImageWidthSmall: 'Širina slike "{name}" mora biti vsaj {size} px.', msgImageHeightSmall: 'Višina slike "{name}" mora biti vsaj {size} px.', msgImageWidthLarge: 'Širina slike "{name}" ne sme preseči {size} px.', msgImageHeightLarge: 'Višina slike "{name}" ne sme preseči {size} px.', msgImageResizeError: 'Nisem mogel pridobiti dimenzij slike za spreminjanje velikosti.', msgImageResizeException: 'Napaka pri spreminjanju velikosti slike.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Povlecite in spustite datoteke sem …', dropZoneClickTitle: '
    (ali kliknite sem za izbiro {files})', fileActionSettings: { removeTitle: 'Odstrani datoteko', uploadTitle: 'Naloži datoteko', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Poglej podrobnosti', dragTitle: 'Premaki / Razporedi', indicatorNewTitle: 'Še ni naloženo', indicatorSuccessTitle: 'Naloženo', indicatorErrorTitle: 'Napaka pri nalaganju', indicatorLoadingTitle: 'Nalagam ...' }, previewZoomButtonTitles: { prev: 'Poglej prejšno datoteko', next: 'Poglej naslednjo datoteko', toggleheader: 'Preklopi glavo', fullscreen: 'Preklopi celozaslonski način', borderless: 'Preklopi način brez robov', close: 'Zapri predogled podrobnosti' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/sv.js ================================================ /*! * FileInput <_LANG_> Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['sv'] = { fileSingle: 'fil', filePlural: 'filer', browseLabel: 'Bläddra …', removeLabel: 'Ta bort', removeTitle: 'Rensa valda filer', cancelLabel: 'Avbryt', cancelTitle: 'Avbryt pågående uppladdning', uploadLabel: 'Ladda upp', uploadTitle: 'Ladda upp valda filer', msgNo: 'Nej', msgNoFilesSelected: 'Inga filer valda', msgCancelled: 'Avbruten', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'detaljerad förhandsgranskning', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'Filen "{name}" ({size} KB) är för liten och måste vara större än {minSize} KB.', msgSizeTooLarge: 'File "{name}" ({size} KB) överstiger högsta tillåtna uppladdningsstorlek {maxSize} KB.', msgFilesTooLess: 'Du måste välja minst {n} {files} för att ladda upp.', msgFilesTooMany: 'Antal filer valda för uppladdning ({n}) överstiger högsta tillåtna gränsen {m}.', msgFileNotFound: 'Filen "{name}" kunde inte hittas!', msgFileSecured: 'Säkerhetsbegränsningar förhindrar att läsa filen "{name}".', msgFileNotReadable: 'Filen "{name}" är inte läsbar.', msgFilePreviewAborted: 'Filförhandsvisning avbröts för "{name}".', msgFilePreviewError: 'Ett fel uppstod vid inläsning av filen "{name}".', msgInvalidFileName: 'Ogiltiga eller tecken som inte stöds i filnamnet "{name}".', msgInvalidFileType: 'Ogiltig typ för filen "{name}". Endast "{types}" filtyper stöds.', msgInvalidFileExtension: 'Ogiltigt filtillägg för filen "{name}". Endast "{extensions}" filer stöds.', msgFileTypes: { 'image': 'bild', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'ljud', 'flash': 'flash', 'pdf': 'PDF', 'object': 'objekt' }, msgUploadAborted: 'Filöverföringen avbröts', msgUploadThreshold: 'Bearbetar...', msgUploadBegin: 'Påbörjar...', msgUploadEnd: 'Färdig', msgUploadEmpty: 'Ingen giltig data tillgänglig för uppladdning.', msgUploadError: 'Error', msgValidationError: 'Valideringsfel', msgLoading: 'Laddar fil {index} av {files} …', msgProgress: 'Laddar fil {index} av {files} - {name} - {percent}% färdig.', msgSelected: '{n} {files} valda', msgFoldersNotAllowed: 'Endast drag & släppfiler! Skippade {n} släpta mappar.', msgImageWidthSmall: 'Bredd på bildfilen "{name}" måste minst vara {size} pixlar.', msgImageHeightSmall: 'Höjden på bildfilen "{name}" måste minst vara {size} pixlar.', msgImageWidthLarge: 'Bredd på bildfil "{name}" kan inte överstiga {size} pixlar.', msgImageHeightLarge: 'Höjden på bildfilen "{name}" kan inte överstiga {size} pixlar.', msgImageResizeError: 'Det gick inte att hämta bildens dimensioner för att ändra storlek.', msgImageResizeException: 'Fel vid storleksändring av bilden.
    {errors}
    ', msgAjaxError: 'Något gick fel med {operation} operationen. Försök igen senare!', msgAjaxProgressError: '{operation} misslyckades', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & släpp filer här …', dropZoneClickTitle: '
    (eller klicka för att markera {files})', fileActionSettings: { removeTitle: 'Ta bort fil', uploadTitle: 'Ladda upp fil', uploadRetryTitle: 'Retry upload', zoomTitle: 'Visa detaljer', dragTitle: 'Flytta / Ändra ordning', indicatorNewTitle: 'Inte uppladdat ännu', indicatorSuccessTitle: 'Uppladdad', indicatorErrorTitle: 'Uppladdningsfel', indicatorLoadingTitle: 'Laddar upp...' }, previewZoomButtonTitles: { prev: 'Visa föregående fil', next: 'Visa nästa fil', toggleheader: 'Rubrik', fullscreen: 'Fullskärm', borderless: 'Gränslös', close: 'Stäng detaljerad förhandsgranskning' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/th.js ================================================ /*! * FileInput Thai Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['th'] = { fileSingle: 'ไฟล์', filePlural: 'ไฟล์', browseLabel: 'เลือกดู …', removeLabel: 'ลบทิ้ง', removeTitle: 'ลบไฟล์ที่เลือกทิ้ง', cancelLabel: 'ยกเลิก', cancelTitle: 'ยกเลิกการอัพโหลด', uploadLabel: 'อัพโหลด', uploadTitle: 'อัพโหลดไฟล์ที่เลือก', msgNo: 'ไม่', msgNoFilesSelected: '', msgCancelled: 'ยกเลิก', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'ตัวอย่างละเอียด', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'ไฟล์ "{name}" ({size} KB) มีขนาดเกินที่ระบบอนุญาตที่ {maxSize} KB, กรุณาลองใหม่อีกครั้ง!', msgFilesTooLess: 'คุณต้องเลือกไฟล์จำนวนอย่างน้อย {n} {files} เพื่ออัพโหลด, กรุณาลองใหม่อีกครั้ง!', msgFilesTooMany: 'ไฟล์ที่คุณเลือกมีจำนวน ({n}) ซึ่งเกินกว่าที่ระบบอนุญาตที่ {m}, กรุณาลองใหม่อีกครั้ง!', msgFileNotFound: 'ไม่พบไฟล์ "{name}" !', msgFileSecured: 'ระบบความปลอดภัยไม่อนุญาตให้อ่านไฟล์ "{name}".', msgFileNotReadable: 'ไม่สามารถอ่านไฟล์ "{name}" ได้', msgFilePreviewAborted: 'ไฟล์ "{name}" ไม่อนุญาตให้ดูตัวอย่าง', msgFilePreviewError: 'พบปัญหาในการดูตัวอย่างไฟล์ "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'ไฟล์ "{name}" เป็นประเภทไฟล์ที่ไม่ถูกต้อง, อนุญาตเฉพาะไฟล์ประเภท "{types}"', msgInvalidFileExtension: 'ไฟล์ "{name}" เป็น extension ที่ไมถูกต้อง, อนุญาตเฉพาะไฟล์ extension "{extensions}"', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'อัปโหลดไฟล์ถูกยกเลิก', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'ข้อผิดพลาดในการตรวจสอบ', msgLoading: 'กำลังโหลดไฟล์ {index} จาก {files} …', msgProgress: 'กำลังโหลดไฟล์ {index} จาก {files} - {name} - {percent}%', msgSelected: '{n} {files} ถูกเลือก', msgFoldersNotAllowed: 'Drag & drop เฉพาะไฟล์เท่านั้น! ข้าม dropped folder จำนวน {n}', msgImageWidthSmall: 'ความกว้างของภาพไฟล์ "{name}" ต้องมีอย่างน้อย {size} px.', msgImageHeightSmall: 'ความสูงของภาพไฟล์ "{name}" ต้องมีอย่างน้อย {size} px.', msgImageWidthLarge: 'ความกว้างของภาพไฟล์ "{name}" ไม่เกิน {size} พิกเซล.', msgImageHeightLarge: 'ความสูงของไฟล์ภาพ "{name}" ไม่เกิน {size} พิกเซล.', msgImageResizeError: 'ไม่สามารถรับขนาดภาพเพื่อปรับขนาด', msgImageResizeException: 'ข้อผิดพลาดขณะปรับขนาดภาพ
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Drag & drop ไฟล์ตรงนี้ …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'ลบไฟล์', uploadTitle: 'อัปโหลดไฟล์', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'ดูรายละเอียด', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'ยังไม่ได้อัปโหลด', indicatorSuccessTitle: 'อัพโหลด', indicatorErrorTitle: 'อัปโหลดข้อผิดพลาด', indicatorLoadingTitle: 'อัพโหลด ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/tr.js ================================================ /*! * FileInput Turkish Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['tr'] = { fileSingle: 'dosya', filePlural: 'dosyalar', browseLabel: 'Gözat …', removeLabel: 'Sil', removeTitle: 'Seçilen dosyaları sil', cancelLabel: 'İptal', cancelTitle: 'Devam eden yüklemeyi iptal et', uploadLabel: 'Yükle', uploadTitle: 'Seçilen dosyaları yükle', msgNo: 'Hayır', msgNoFilesSelected: '', msgCancelled: 'İptal edildi', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Detaylı Önizleme', msgFileRequired: 'Yüklemek için bir dosya seçmelisiniz.', msgSizeTooSmall: '"{name}"({size} KB) dosyası çok küçük ve {minSize} KB boyutundan büyük olmalıdır.', msgSizeTooLarge: '"{name}" dosyasının boyutu ({size} KB) izin verilen azami dosya boyutu olan {maxSize} KB\'tan büyük.', msgFilesTooLess: 'Yüklemek için en az {n} {files} dosya seçmelisiniz.', msgFilesTooMany: 'Yüklemek için seçtiğiniz dosya sayısı ({n}) azami limitin ({m}) altında olmalıdır.', msgFileNotFound: '"{name}" dosyası bulunamadı!', msgFileSecured: 'Güvenlik kısıtlamaları "{name}" dosyasının okunmasını engelliyor.', msgFileNotReadable: '"{name}" dosyası okunabilir değil.', msgFilePreviewAborted: '"{name}" dosyası için önizleme iptal edildi.', msgFilePreviewError: '"{name}" dosyası okunurken bir hata oluştu.', msgInvalidFileName: '"{name}" dosya adında geçersiz veya desteklenmeyen karakterler var.', msgInvalidFileType: '"{name}" dosyasının türü geçerli değil. Yalnızca "{types}" türünde dosyalara izin veriliyor.', msgInvalidFileExtension: '"{name}" dosyasının uzantısı geçersiz. Yalnızca "{extensions}" uzantılı dosyalara izin veriliyor.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Dosya yükleme iptal edildi', msgUploadThreshold: 'İşlem yapılıyor...', msgUploadBegin: 'Başlıyor...', msgUploadEnd: 'Başarılı', msgUploadEmpty: 'Yüklemek için geçerli veri mevcut değil.', msgUploadError: 'Error', msgValidationError: 'Doğrulama Hatası', msgLoading: 'Dosya yükleniyor {index} / {files} …', msgProgress: 'Dosya yükleniyor {index} / {files} - {name} - %{percent} tamamlandı.', msgSelected: '{n} {files} seçildi', msgFoldersNotAllowed: 'Yalnızca dosyaları sürükleyip bırakabilirsiniz! {n} dizin(ler) göz ardı edildi.', msgImageWidthSmall: '"{name}" adlı görüntü dosyasının genişliği en az {size} piksel olmalıdır.', msgImageHeightSmall: '"{name}" adlı görüntü dosyasının yüksekliği en az {size} piksel olmalıdır.', msgImageWidthLarge: '"{name}" adlı görüntü dosyasının genişliği {size} pikseli geçemez.', msgImageHeightLarge: '"{name}" adlı görüntü dosyasının yüksekliği {size} pikseli geçemez.', msgImageResizeError: 'Görüntü boyutlarını yeniden boyutlandıramadı.', msgImageResizeException: 'Görüntü boyutlandırma sırasında hata.
    {errors}
    ', msgAjaxError: '{operation} işlemi ile ilgili bir şeyler ters gitti. Lütfen daha sonra tekrar deneyiniz!', msgAjaxProgressError: '{operation} işlemi başarısız oldu.', ajaxOperations: { deleteThumb: 'dosya silme', uploadThumb: 'dosya yükleme', uploadBatch: 'toplu dosya yükleme', uploadExtra: 'form verisi yükleme' }, dropZoneTitle: 'Dosyaları buraya sürükleyip bırakın', dropZoneClickTitle: '
    (ya da {files} seçmek için tıklayınız)', fileActionSettings: { removeTitle: 'Dosyayı kaldır', uploadTitle: 'Dosyayı yükle', uploadRetryTitle: 'Retry upload', zoomTitle: 'Ayrıntıları görüntüle', dragTitle: 'Taşı / Yeniden düzenle', indicatorNewTitle: 'Henüz yüklenmedi', indicatorSuccessTitle: 'Yüklendi', indicatorErrorTitle: 'Yükleme Hatası', indicatorLoadingTitle: 'Yükleniyor ...' }, previewZoomButtonTitles: { prev: 'Önceki dosyayı göster', next: 'Sonraki dosyayı göster', toggleheader: 'Üst bilgi geçiş', fullscreen: 'Tam ekran geçiş', borderless: 'Çerçevesiz moda geçiş', close: 'Detaylı önizlemeyi kapat' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/uk.js ================================================ /*! * FileInput Ukrainian Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author CyanoFresh * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['uk'] = { fileSingle: 'файл', filePlural: 'файли', browseLabel: 'Вибрати …', removeLabel: 'Видалити', removeTitle: 'Видалити вибрані файли', cancelLabel: 'Скасувати', cancelTitle: 'Скасувати поточну загрузку', uploadLabel: 'Загрузити', uploadTitle: 'Загрузити вибрані файли', msgNo: 'Немає', msgNoFilesSelected: '', msgCancelled: 'Cкасовано', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Детальний превью', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Файл "{name}" ({size} KB) перевищує максимальний розмір {maxSize} KB.', msgFilesTooLess: 'Ви повинні вибрати як мінімум {n} {files} для загрузки.', msgFilesTooMany: 'Кількість вибраних файлів ({n}) перевищує максимально допустиму кількість {m}.', msgFileNotFound: 'Файл "{name}" не знайдено!', msgFileSecured: 'Обмеження безпеки перешкоджають читанню файла "{name}".', msgFileNotReadable: 'Файл "{name}" неможливо прочитати.', msgFilePreviewAborted: 'Перегляд скасований для файла "{name}".', msgFilePreviewError: 'Сталася помилка під час читання файла "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Заборонений тип файла для "{name}". Тільки "{types}" дозволені.', msgInvalidFileExtension: 'Заборонене розширення для файла "{name}". Тільки "{extensions}" дозволені.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Вивантаження файлу перервана', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Помилка перевірки', msgLoading: 'Загрузка файла {index} із {files} …', msgProgress: 'Загрузка файла {index} із {files} - {name} - {percent}% завершено.', msgSelected: '{n} {files} вибрано', msgFoldersNotAllowed: 'Дозволено перетягувати тільки файли! Пропущено {n} папок.', msgImageWidthSmall: 'Ширина зображення "{name}" повинна бути не менше {size} px.', msgImageHeightSmall: 'Висота зображення "{name}" повинна бути не менше {size} px.', msgImageWidthLarge: 'Ширина зображення "{name}" не може перевищувати {size} px.', msgImageHeightLarge: 'Висота зображення "{name}" не може перевищувати {size} px.', msgImageResizeError: 'Не вдалося розміри зображення, щоб змінити розмір.', msgImageResizeException: 'Помилка при зміні розміру зображення.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Перетягніть файли сюди …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: 'Видалити файл', uploadTitle: 'Загрузити файл', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Подивитися деталі', dragTitle: 'Move / Rearrange', indicatorNewTitle: 'Ще не загружено', indicatorSuccessTitle: 'Загружено', indicatorErrorTitle: 'Помилка при загрузці', indicatorLoadingTitle: 'Загрузка ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/vi.js ================================================ /*! * FileInput Vietnamese Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['vi'] = { fileSingle: 'tập tin', filePlural: 'các tập tin', browseLabel: 'Duyệt …', removeLabel: 'Gỡ bỏ', removeTitle: 'Bỏ tập tin đã chọn', cancelLabel: 'Hủy', cancelTitle: 'Hủy upload', uploadLabel: 'Upload', uploadTitle: 'Upload tập tin đã chọn', msgNo: 'Không', msgNoFilesSelected: 'Không tập tin nào được chọn', msgCancelled: 'Đã hủy', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: 'Chi tiết xem trước', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: 'Tập tin "{name}" ({size} KB) vượt quá kích thước giới hạn cho phép {maxSize} KB.', msgFilesTooLess: 'Bạn phải chọn ít nhất {n} {files} để upload.', msgFilesTooMany: 'Số lượng tập tin upload ({n}) vượt quá giới hạn cho phép là {m}.', msgFileNotFound: 'Không tìm thấy tập tin "{name}"!', msgFileSecured: 'Các hạn chế về bảo mật không cho phép đọc tập tin "{name}".', msgFileNotReadable: 'Không đọc được tập tin "{name}".', msgFilePreviewAborted: 'Đã dừng xem trước tập tin "{name}".', msgFilePreviewError: 'Đã xảy ra lỗi khi đọc tập tin "{name}".', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: 'Tập tin "{name}" không hợp lệ. Chỉ hỗ trợ loại tập tin "{types}".', msgInvalidFileExtension: 'Phần mở rộng của tập tin "{name}" không hợp lệ. Chỉ hỗ trợ phần mở rộng "{extensions}".', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: 'Đã dừng upload', msgUploadThreshold: 'Đang xử lý...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: 'Lỗi xác nhận', msgLoading: 'Đang nạp {index} tập tin trong số {files} …', msgProgress: 'Đang nạp {index} tập tin trong số {files} - {name} - {percent}% hoàn thành.', msgSelected: '{n} {files} được chọn', msgFoldersNotAllowed: 'Chỉ kéo thả tập tin! Đã bỏ qua {n} thư mục.', msgImageWidthSmall: 'Chiều rộng của hình ảnh "{name}" phải tối thiểu là {size} px.', msgImageHeightSmall: 'Chiều cao của hình ảnh "{name}" phải tối thiểu là {size} px.', msgImageWidthLarge: 'Chiều rộng của hình ảnh "{name}" không được quá {size} px.', msgImageHeightLarge: 'Chiều cao của hình ảnh "{name}" không được quá {size} px.', msgImageResizeError: 'Không lấy được kích thước của hình ảnh để resize.', msgImageResizeException: 'Resize hình ảnh bị lỗi.
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: 'Kéo thả tập tin vào đây …', dropZoneClickTitle: '
    (hoặc click để chọn {files})', fileActionSettings: { removeTitle: 'Gỡ bỏ', uploadTitle: 'Upload tập tin', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: 'Phóng lớn', dragTitle: 'Di chuyển / Sắp xếp lại', indicatorNewTitle: 'Chưa được upload', indicatorSuccessTitle: 'Đã upload', indicatorErrorTitle: 'Upload bị lỗi', indicatorLoadingTitle: 'Đang upload ...' }, previewZoomButtonTitles: { prev: 'Xem tập tin phía trước', next: 'Xem tập tin tiếp theo', toggleheader: 'Ẩn/hiện tiêu đề', fullscreen: 'Bật/tắt toàn màn hình', borderless: 'Bật/tắt chế độ không viền', close: 'Đóng' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/zh-TW.js ================================================ /*! * FileInput Chinese Traditional Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author kangqf * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['zh-TW'] = { fileSingle: '單一檔案', filePlural: '複選檔案', browseLabel: '瀏覽 …', removeLabel: '移除', removeTitle: '清除選取檔案', cancelLabel: '取消', cancelTitle: '取消上傳中檔案', uploadLabel: '上傳', uploadTitle: '上傳選取檔案', msgNo: '沒有', msgNoFilesSelected: '', msgCancelled: '取消', zoomTitle: '詳細資料', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: '內容預覽', msgFileRequired: 'You must select a file to upload.', msgSizeTooSmall: 'File "{name}" ({size} KB) is too small and must be larger than {minSize} KB.', msgSizeTooLarge: '檔案 "{name}" ({size} KB) 大小超過上限 {maxSize} KB.', msgFilesTooLess: '最少必須選擇 {n} {files} 來上傳. ', msgFilesTooMany: '上傳的檔案數量 ({n}) 超過最大檔案上傳限制 {m}.', msgFileNotFound: '檔案 "{name}" 未發現!', msgFileSecured: '安全限制,禁止讀取檔案 "{name}".', msgFileNotReadable: '文件 "{name}" 不可讀取.', msgFilePreviewAborted: '檔案 "{name}" 預覽中止.', msgFilePreviewError: '讀取 "{name}" 發生錯誤.', msgInvalidFileName: 'Invalid or unsupported characters in file name "{name}".', msgInvalidFileType: '檔案類型錯誤 "{name}". 只能使用 "{types}" 類型的檔案.', msgInvalidFileExtension: '附檔名錯誤 "{name}". 只能使用 "{extensions}" 的檔案.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: '該文件上傳被中止', msgUploadThreshold: 'Processing...', msgUploadBegin: 'Initializing...', msgUploadEnd: 'Done', msgUploadEmpty: 'No valid data available for upload.', msgUploadError: 'Error', msgValidationError: '驗證錯誤', msgLoading: '載入第 {index} 個檔案,共 {files} …', msgProgress: '載入第 {index} 個檔案,共 {files} - {name} - {percent}% 成功.', msgSelected: '{n} {files} 選取', msgFoldersNotAllowed: '只支援單檔拖曳! 無法使用 {n} 拖拽的資料夹.', msgImageWidthSmall: '圖檔寬度"{name}"必須至少為{size}像素(px).', msgImageHeightSmall: '圖檔高度"{name}"必須至少為{size}像素(px).', msgImageWidthLarge: '圖檔寬度"{name}"不能超過{size}像素(px).', msgImageHeightLarge: '圖檔高度"{name}"不能超過{size}像素(px).', msgImageResizeError: '無法獲取的圖像尺寸調整。', msgImageResizeException: '錯誤而調整圖像大小。
    {errors}
    ', msgAjaxError: 'Something went wrong with the {operation} operation. Please try again later!', msgAjaxProgressError: '{operation} failed', ajaxOperations: { deleteThumb: 'file delete', uploadThumb: 'file upload', uploadBatch: 'batch file upload', uploadExtra: 'form data upload' }, dropZoneTitle: '拖曳檔案至此 …', dropZoneClickTitle: '
    (or click to select {files})', fileActionSettings: { removeTitle: '刪除檔案', uploadTitle: '上傳檔案', uploadRetryTitle: 'Retry upload', downloadTitle: 'Download file', zoomTitle: '詳細資料', dragTitle: 'Move / Rearrange', indicatorNewTitle: '尚未上傳', indicatorSuccessTitle: '上傳成功', indicatorErrorTitle: '上傳失敗', indicatorLoadingTitle: '上傳中 ...' }, previewZoomButtonTitles: { prev: 'View previous file', next: 'View next file', toggleheader: 'Toggle header', fullscreen: 'Toggle full screen', borderless: 'Toggle borderless mode', close: 'Close detailed preview' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/zh.js ================================================ /*! * FileInput Chinese Translations * * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or * any HTML markup tags in the messages must not be converted or translated. * * @see http://github.com/kartik-v/bootstrap-fileinput * @author kangqf * * NOTE: this file must be saved in UTF-8 encoding. */ (function ($) { "use strict"; $.fn.fileinputLocales['zh'] = { fileSingle: '文件', filePlural: '个文件', browseLabel: '选择 …', removeLabel: '移除', removeTitle: '清除选中文件', cancelLabel: '取消', cancelTitle: '取消进行中的上传', uploadLabel: '上传', uploadTitle: '上传选中文件', msgNo: '没有', msgNoFilesSelected: '', msgCancelled: '取消', msgPlaceholder: 'Select {files}...', msgZoomModalHeading: '详细预览', msgFileRequired: '必须选择一个文件上传.', msgSizeTooSmall: '文件 "{name}" ({size} KB) 必须大于限定大小 {minSize} KB.', msgSizeTooLarge: '文件 "{name}" ({size} KB) 超过了允许大小 {maxSize} KB.', msgFilesTooLess: '你必须选择最少 {n} {files} 来上传. ', msgFilesTooMany: '选择的上传文件个数 ({n}) 超出最大文件的限制个数 {m}.', msgFileNotFound: '文件 "{name}" 未找到!', msgFileSecured: '安全限制,为了防止读取文件 "{name}".', msgFileNotReadable: '文件 "{name}" 不可读.', msgFilePreviewAborted: '取消 "{name}" 的预览.', msgFilePreviewError: '读取 "{name}" 时出现了一个错误.', msgInvalidFileName: '文件名 "{name}" 包含非法字符.', msgInvalidFileType: '不正确的类型 "{name}". 只支持 "{types}" 类型的文件.', msgInvalidFileExtension: '不正确的文件扩展名 "{name}". 只支持 "{extensions}" 的文件扩展名.', msgFileTypes: { 'image': 'image', 'html': 'HTML', 'text': 'text', 'video': 'video', 'audio': 'audio', 'flash': 'flash', 'pdf': 'PDF', 'object': 'object' }, msgUploadAborted: '该文件上传被中止', msgUploadThreshold: '处理中...', msgUploadBegin: '正在初始化...', msgUploadEnd: '完成', msgUploadEmpty: '无效的文件上传.', msgUploadError: 'Error', msgValidationError: '验证错误', msgLoading: '加载第 {index} 文件 共 {files} …', msgProgress: '加载第 {index} 文件 共 {files} - {name} - {percent}% 完成.', msgSelected: '{n} {files} 选中', msgFoldersNotAllowed: '只支持拖拽文件! 跳过 {n} 拖拽的文件夹.', msgImageWidthSmall: '图像文件的"{name}"的宽度必须是至少{size}像素.', msgImageHeightSmall: '图像文件的"{name}"的高度必须至少为{size}像素.', msgImageWidthLarge: '图像文件"{name}"的宽度不能超过{size}像素.', msgImageHeightLarge: '图像文件"{name}"的高度不能超过{size}像素.', msgImageResizeError: '无法获取的图像尺寸调整。', msgImageResizeException: '调整图像大小时发生错误。
    {errors}
    ', msgAjaxError: '{operation} 发生错误. 请重试!', msgAjaxProgressError: '{operation} 失败', ajaxOperations: { deleteThumb: '删除文件', uploadThumb: '上传文件', uploadBatch: '批量上传', uploadExtra: '表单数据上传' }, dropZoneTitle: '拖拽文件到这里 …
    支持多文件同时上传', dropZoneClickTitle: '
    (或点击{files}按钮选择文件)', fileActionSettings: { removeTitle: '删除文件', uploadTitle: '上传文件', uploadRetryTitle: 'Retry upload', zoomTitle: '查看详情', dragTitle: '移动 / 重置', indicatorNewTitle: '没有上传', indicatorSuccessTitle: '上传', indicatorErrorTitle: '上传错误', indicatorLoadingTitle: '上传 ...' }, previewZoomButtonTitles: { prev: '预览上一个文件', next: '预览下一个文件', toggleheader: '缩放', fullscreen: '全屏', borderless: '无边界模式', close: '关闭当前预览' } }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/plugins/piexif.js ================================================ /* piexifjs The MIT License (MIT) Copyright (c) 2014, 2015 hMatoba(https://github.com/hMatoba) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function () { "use strict"; var that = {}; that.version = "1.03"; that.remove = function (jpeg) { var b64 = false; if (jpeg.slice(0, 2) == "\xff\xd8") { } else if (jpeg.slice(0, 23) == "data:image/jpeg;base64," || jpeg.slice(0, 22) == "data:image/jpg;base64,") { jpeg = atob(jpeg.split(",")[1]); b64 = true; } else { throw ("Given data is not jpeg."); } var segments = splitIntoSegments(jpeg); if (segments[1].slice(0, 2) == "\xff\xe1" && segments[1].slice(4, 10) == "Exif\x00\x00") { segments = [segments[0]].concat(segments.slice(2)); } else if (segments[2].slice(0, 2) == "\xff\xe1" && segments[2].slice(4, 10) == "Exif\x00\x00") { segments = segments.slice(0, 2).concat(segments.slice(3)); } else { throw("Exif not found."); } var new_data = segments.join(""); if (b64) { new_data = "data:image/jpeg;base64," + btoa(new_data); } return new_data; }; that.insert = function (exif, jpeg) { var b64 = false; if (exif.slice(0, 6) != "\x45\x78\x69\x66\x00\x00") { throw ("Given data is not exif."); } if (jpeg.slice(0, 2) == "\xff\xd8") { } else if (jpeg.slice(0, 23) == "data:image/jpeg;base64," || jpeg.slice(0, 22) == "data:image/jpg;base64,") { jpeg = atob(jpeg.split(",")[1]); b64 = true; } else { throw ("Given data is not jpeg."); } var exifStr = "\xff\xe1" + pack(">H", [exif.length + 2]) + exif; var segments = splitIntoSegments(jpeg); var new_data = mergeSegments(segments, exifStr); if (b64) { new_data = "data:image/jpeg;base64," + btoa(new_data); } return new_data; }; that.load = function (data) { var input_data; if (typeof (data) == "string") { if (data.slice(0, 2) == "\xff\xd8") { input_data = data; } else if (data.slice(0, 23) == "data:image/jpeg;base64," || data.slice(0, 22) == "data:image/jpg;base64,") { input_data = atob(data.split(",")[1]); } else if (data.slice(0, 4) == "Exif") { input_data = data.slice(6); } else { throw ("'load' gots invalid file data."); } } else { throw ("'load' gots invalid type argument."); } var exifDict = {}; var exif_dict = { "0th": {}, "Exif": {}, "GPS": {}, "Interop": {}, "1st": {}, "thumbnail": null }; var exifReader = new ExifReader(input_data); if (exifReader.tiftag === null) { return exif_dict; } if (exifReader.tiftag.slice(0, 2) == "\x49\x49") { exifReader.endian_mark = "<"; } else { exifReader.endian_mark = ">"; } var pointer = unpack(exifReader.endian_mark + "L", exifReader.tiftag.slice(4, 8))[0]; exif_dict["0th"] = exifReader.get_ifd(pointer, "0th"); var first_ifd_pointer = exif_dict["0th"]["first_ifd_pointer"]; delete exif_dict["0th"]["first_ifd_pointer"]; if (34665 in exif_dict["0th"]) { pointer = exif_dict["0th"][34665]; exif_dict["Exif"] = exifReader.get_ifd(pointer, "Exif"); } if (34853 in exif_dict["0th"]) { pointer = exif_dict["0th"][34853]; exif_dict["GPS"] = exifReader.get_ifd(pointer, "GPS"); } if (40965 in exif_dict["Exif"]) { pointer = exif_dict["Exif"][40965]; exif_dict["Interop"] = exifReader.get_ifd(pointer, "Interop"); } if (first_ifd_pointer != "\x00\x00\x00\x00") { pointer = unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0]; exif_dict["1st"] = exifReader.get_ifd(pointer, "1st"); if ((513 in exif_dict["1st"]) && (514 in exif_dict["1st"])) { var end = exif_dict["1st"][513] + exif_dict["1st"][514]; var thumb = exifReader.tiftag.slice(exif_dict["1st"][513], end); exif_dict["thumbnail"] = thumb; } } return exif_dict; }; that.dump = function (exif_dict_original) { var TIFF_HEADER_LENGTH = 8; var exif_dict = copy(exif_dict_original); var header = "Exif\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08"; var exif_is = false; var gps_is = false; var interop_is = false; var first_is = false; var zeroth_ifd, exif_ifd, interop_ifd, gps_ifd, first_ifd; if ("0th" in exif_dict) { zeroth_ifd = exif_dict["0th"]; } else { zeroth_ifd = {}; } if ((("Exif" in exif_dict) && (Object.keys(exif_dict["Exif"]).length)) || (("Interop" in exif_dict) && (Object.keys(exif_dict["Interop"]).length))) { zeroth_ifd[34665] = 1; exif_is = true; exif_ifd = exif_dict["Exif"]; if (("Interop" in exif_dict) && Object.keys(exif_dict["Interop"]).length) { exif_ifd[40965] = 1; interop_is = true; interop_ifd = exif_dict["Interop"]; } else if (Object.keys(exif_ifd).indexOf(that.ExifIFD.InteroperabilityTag.toString()) > -1) { delete exif_ifd[40965]; } } else if (Object.keys(zeroth_ifd).indexOf(that.ImageIFD.ExifTag.toString()) > -1) { delete zeroth_ifd[34665]; } if (("GPS" in exif_dict) && (Object.keys(exif_dict["GPS"]).length)) { zeroth_ifd[that.ImageIFD.GPSTag] = 1; gps_is = true; gps_ifd = exif_dict["GPS"]; } else if (Object.keys(zeroth_ifd).indexOf(that.ImageIFD.GPSTag.toString()) > -1) { delete zeroth_ifd[that.ImageIFD.GPSTag]; } if (("1st" in exif_dict) && ("thumbnail" in exif_dict) && (exif_dict["thumbnail"] != null)) { first_is = true; exif_dict["1st"][513] = 1; exif_dict["1st"][514] = 1; first_ifd = exif_dict["1st"]; } var zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0); var zeroth_length = (zeroth_set[0].length + exif_is * 12 + gps_is * 12 + 4 + zeroth_set[1].length); var exif_set, exif_bytes = "", exif_length = 0, gps_set, gps_bytes = "", gps_length = 0, interop_set, interop_bytes = "", interop_length = 0, first_set, first_bytes = "", thumbnail; if (exif_is) { exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length); exif_length = exif_set[0].length + interop_is * 12 + exif_set[1].length; } if (gps_is) { gps_set = _dict_to_bytes(gps_ifd, "GPS", zeroth_length + exif_length); gps_bytes = gps_set.join(""); gps_length = gps_bytes.length; } if (interop_is) { var offset = zeroth_length + exif_length + gps_length; interop_set = _dict_to_bytes(interop_ifd, "Interop", offset); interop_bytes = interop_set.join(""); interop_length = interop_bytes.length; } if (first_is) { var offset = zeroth_length + exif_length + gps_length + interop_length; first_set = _dict_to_bytes(first_ifd, "1st", offset); thumbnail = _get_thumbnail(exif_dict["thumbnail"]); if (thumbnail.length > 64000) { throw ("Given thumbnail is too large. max 64kB"); } } var exif_pointer = "", gps_pointer = "", interop_pointer = "", first_ifd_pointer = "\x00\x00\x00\x00"; if (exif_is) { var pointer_value = TIFF_HEADER_LENGTH + zeroth_length; var pointer_str = pack(">L", [pointer_value]); var key = 34665; var key_str = pack(">H", [key]); var type_str = pack(">H", [TYPES["Long"]]); var length_str = pack(">L", [1]); exif_pointer = key_str + type_str + length_str + pointer_str; } if (gps_is) { var pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length; var pointer_str = pack(">L", [pointer_value]); var key = 34853; var key_str = pack(">H", [key]); var type_str = pack(">H", [TYPES["Long"]]); var length_str = pack(">L", [1]); gps_pointer = key_str + type_str + length_str + pointer_str; } if (interop_is) { var pointer_value = (TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length); var pointer_str = pack(">L", [pointer_value]); var key = 40965; var key_str = pack(">H", [key]); var type_str = pack(">H", [TYPES["Long"]]); var length_str = pack(">L", [1]); interop_pointer = key_str + type_str + length_str + pointer_str; } if (first_is) { var pointer_value = (TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length + interop_length); first_ifd_pointer = pack(">L", [pointer_value]); var thumbnail_pointer = (pointer_value + first_set[0].length + 24 + 4 + first_set[1].length); var thumbnail_p_bytes = ("\x02\x01\x00\x04\x00\x00\x00\x01" + pack(">L", [thumbnail_pointer])); var thumbnail_length_bytes = ("\x02\x02\x00\x04\x00\x00\x00\x01" + pack(">L", [thumbnail.length])); first_bytes = (first_set[0] + thumbnail_p_bytes + thumbnail_length_bytes + "\x00\x00\x00\x00" + first_set[1] + thumbnail); } var zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1]); if (exif_is) { exif_bytes = exif_set[0] + interop_pointer + exif_set[1]; } return (header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes); }; function copy(obj) { return JSON.parse(JSON.stringify(obj)); } function _get_thumbnail(jpeg) { var segments = splitIntoSegments(jpeg); while (("\xff\xe0" <= segments[1].slice(0, 2)) && (segments[1].slice(0, 2) <= "\xff\xef")) { segments = [segments[0]].concat(segments.slice(2)); } return segments.join(""); } function _pack_byte(array) { return pack(">" + nStr("B", array.length), array); } function _pack_short(array) { return pack(">" + nStr("H", array.length), array); } function _pack_long(array) { return pack(">" + nStr("L", array.length), array); } function _value_to_bytes(raw_value, value_type, offset) { var four_bytes_over = ""; var value_str = ""; var length, new_value, num, den; if (value_type == "Byte") { length = raw_value.length; if (length <= 4) { value_str = (_pack_byte(raw_value) + nStr("\x00", 4 - length)); } else { value_str = pack(">L", [offset]); four_bytes_over = _pack_byte(raw_value); } } else if (value_type == "Short") { length = raw_value.length; if (length <= 2) { value_str = (_pack_short(raw_value) + nStr("\x00\x00", 2 - length)); } else { value_str = pack(">L", [offset]); four_bytes_over = _pack_short(raw_value); } } else if (value_type == "Long") { length = raw_value.length; if (length <= 1) { value_str = _pack_long(raw_value); } else { value_str = pack(">L", [offset]); four_bytes_over = _pack_long(raw_value); } } else if (value_type == "Ascii") { new_value = raw_value + "\x00"; length = new_value.length; if (length > 4) { value_str = pack(">L", [offset]); four_bytes_over = new_value; } else { value_str = new_value + nStr("\x00", 4 - length); } } else if (value_type == "Rational") { if (typeof (raw_value[0]) == "number") { length = 1; num = raw_value[0]; den = raw_value[1]; new_value = pack(">L", [num]) + pack(">L", [den]); } else { length = raw_value.length; new_value = ""; for (var n = 0; n < length; n++) { num = raw_value[n][0]; den = raw_value[n][1]; new_value += (pack(">L", [num]) + pack(">L", [den])); } } value_str = pack(">L", [offset]); four_bytes_over = new_value; } else if (value_type == "SRational") { if (typeof (raw_value[0]) == "number") { length = 1; num = raw_value[0]; den = raw_value[1]; new_value = pack(">l", [num]) + pack(">l", [den]); } else { length = raw_value.length; new_value = ""; for (var n = 0; n < length; n++) { num = raw_value[n][0]; den = raw_value[n][1]; new_value += (pack(">l", [num]) + pack(">l", [den])); } } value_str = pack(">L", [offset]); four_bytes_over = new_value; } else if (value_type == "Undefined") { length = raw_value.length; if (length > 4) { value_str = pack(">L", [offset]); four_bytes_over = raw_value; } else { value_str = raw_value + nStr("\x00", 4 - length); } } var length_str = pack(">L", [length]); return [length_str, value_str, four_bytes_over]; } function _dict_to_bytes(ifd_dict, ifd, ifd_offset) { var TIFF_HEADER_LENGTH = 8; var tag_count = Object.keys(ifd_dict).length; var entry_header = pack(">H", [tag_count]); var entries_length; if (["0th", "1st"].indexOf(ifd) > -1) { entries_length = 2 + tag_count * 12 + 4; } else { entries_length = 2 + tag_count * 12; } var entries = ""; var values = ""; var key; for (var key in ifd_dict) { if (typeof (key) == "string") { key = parseInt(key); } if ((ifd == "0th") && ([34665, 34853].indexOf(key) > -1)) { continue; } else if ((ifd == "Exif") && (key == 40965)) { continue; } else if ((ifd == "1st") && ([513, 514].indexOf(key) > -1)) { continue; } var raw_value = ifd_dict[key]; var key_str = pack(">H", [key]); var value_type = TAGS[ifd][key]["type"]; var type_str = pack(">H", [TYPES[value_type]]); if (typeof (raw_value) == "number") { raw_value = [raw_value]; } var offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + values.length; var b = _value_to_bytes(raw_value, value_type, offset); var length_str = b[0]; var value_str = b[1]; var four_bytes_over = b[2]; entries += key_str + type_str + length_str + value_str; values += four_bytes_over; } return [entry_header + entries, values]; } function ExifReader(data) { var segments, app1; if (data.slice(0, 2) == "\xff\xd8") { // JPEG segments = splitIntoSegments(data); app1 = getExifSeg(segments); if (app1) { this.tiftag = app1.slice(10); } else { this.tiftag = null; } } else if (["\x49\x49", "\x4d\x4d"].indexOf(data.slice(0, 2)) > -1) { // TIFF this.tiftag = data; } else if (data.slice(0, 4) == "Exif") { // Exif this.tiftag = data.slice(6); } else { throw ("Given file is neither JPEG nor TIFF."); } } ExifReader.prototype = { get_ifd: function (pointer, ifd_name) { var ifd_dict = {}; var tag_count = unpack(this.endian_mark + "H", this.tiftag.slice(pointer, pointer + 2))[0]; var offset = pointer + 2; var t; if (["0th", "1st"].indexOf(ifd_name) > -1) { t = "Image"; } else { t = ifd_name; } for (var x = 0; x < tag_count; x++) { pointer = offset + 12 * x; var tag = unpack(this.endian_mark + "H", this.tiftag.slice(pointer, pointer + 2))[0]; var value_type = unpack(this.endian_mark + "H", this.tiftag.slice(pointer + 2, pointer + 4))[0]; var value_num = unpack(this.endian_mark + "L", this.tiftag.slice(pointer + 4, pointer + 8))[0]; var value = this.tiftag.slice(pointer + 8, pointer + 12); var v_set = [value_type, value_num, value]; if (tag in TAGS[t]) { ifd_dict[tag] = this.convert_value(v_set); } } if (ifd_name == "0th") { pointer = offset + 12 * tag_count; ifd_dict["first_ifd_pointer"] = this.tiftag.slice(pointer, pointer + 4); } return ifd_dict; }, convert_value: function (val) { var data = null; var t = val[0]; var length = val[1]; var value = val[2]; var pointer; if (t == 1) { // BYTE if (length > 4) { pointer = unpack(this.endian_mark + "L", value)[0]; data = unpack(this.endian_mark + nStr("B", length), this.tiftag.slice(pointer, pointer + length)); } else { data = unpack(this.endian_mark + nStr("B", length), value.slice(0, length)); } } else if (t == 2) { // ASCII if (length > 4) { pointer = unpack(this.endian_mark + "L", value)[0]; data = this.tiftag.slice(pointer, pointer + length - 1); } else { data = value.slice(0, length - 1); } } else if (t == 3) { // SHORT if (length > 2) { pointer = unpack(this.endian_mark + "L", value)[0]; data = unpack(this.endian_mark + nStr("H", length), this.tiftag.slice(pointer, pointer + length * 2)); } else { data = unpack(this.endian_mark + nStr("H", length), value.slice(0, length * 2)); } } else if (t == 4) { // LONG if (length > 1) { pointer = unpack(this.endian_mark + "L", value)[0]; data = unpack(this.endian_mark + nStr("L", length), this.tiftag.slice(pointer, pointer + length * 4)); } else { data = unpack(this.endian_mark + nStr("L", length), value); } } else if (t == 5) { // RATIONAL pointer = unpack(this.endian_mark + "L", value)[0]; if (length > 1) { data = []; for (var x = 0; x < length; x++) { data.push([unpack(this.endian_mark + "L", this.tiftag.slice(pointer + x * 8, pointer + 4 + x * 8))[0], unpack(this.endian_mark + "L", this.tiftag.slice(pointer + 4 + x * 8, pointer + 8 + x * 8))[0] ]); } } else { data = [unpack(this.endian_mark + "L", this.tiftag.slice(pointer, pointer + 4))[0], unpack(this.endian_mark + "L", this.tiftag.slice(pointer + 4, pointer + 8))[0] ]; } } else if (t == 7) { // UNDEFINED BYTES if (length > 4) { pointer = unpack(this.endian_mark + "L", value)[0]; data = this.tiftag.slice(pointer, pointer + length); } else { data = value.slice(0, length); } } else if (t == 10) { // SRATIONAL pointer = unpack(this.endian_mark + "L", value)[0]; if (length > 1) { data = []; for (var x = 0; x < length; x++) { data.push([unpack(this.endian_mark + "l", this.tiftag.slice(pointer + x * 8, pointer + 4 + x * 8))[0], unpack(this.endian_mark + "l", this.tiftag.slice(pointer + 4 + x * 8, pointer + 8 + x * 8))[0] ]); } } else { data = [unpack(this.endian_mark + "l", this.tiftag.slice(pointer, pointer + 4))[0], unpack(this.endian_mark + "l", this.tiftag.slice(pointer + 4, pointer + 8))[0] ]; } } else { throw ("Exif might be wrong. Got incorrect value " + "type to decode. type:" + t); } if ((data instanceof Array) && (data.length == 1)) { return data[0]; } else { return data; } }, }; if (typeof window !== "undefined" && typeof window.btoa === "function") { var btoa = window.btoa; } if (typeof btoa === "undefined") { var btoa = function (input) { var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); } return output; }; } if (typeof window !== "undefined" && typeof window.atob === "function") { var atob = window.atob; } if (typeof atob === "undefined") { var atob = function (input) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = keyStr.indexOf(input.charAt(i++)); enc2 = keyStr.indexOf(input.charAt(i++)); enc3 = keyStr.indexOf(input.charAt(i++)); enc4 = keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } return output; }; } function getImageSize(imageArray) { var segments = slice2Segments(imageArray); var seg, width, height, SOF = [192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]; for (var x = 0; x < segments.length; x++) { seg = segments[x]; if (SOF.indexOf(seg[1]) >= 0) { height = seg[5] * 256 + seg[6]; width = seg[7] * 256 + seg[8]; break; } } return [width, height]; } function pack(mark, array) { if (!(array instanceof Array)) { throw ("'pack' error. Got invalid type argument."); } if ((mark.length - 1) != array.length) { throw ("'pack' error. " + (mark.length - 1) + " marks, " + array.length + " elements."); } var littleEndian; if (mark[0] == "<") { littleEndian = true; } else if (mark[0] == ">") { littleEndian = false; } else { throw (""); } var packed = ""; var p = 1; var val = null; var c = null; var valStr = null; while (c = mark[p]) { if (c.toLowerCase() == "b") { val = array[p - 1]; if ((c == "b") && (val < 0)) { val += 0x100; } if ((val > 0xff) || (val < 0)) { throw ("'pack' error."); } else { valStr = String.fromCharCode(val); } } else if (c == "H") { val = array[p - 1]; if ((val > 0xffff) || (val < 0)) { throw ("'pack' error."); } else { valStr = String.fromCharCode(Math.floor((val % 0x10000) / 0x100)) + String.fromCharCode(val % 0x100); if (littleEndian) { valStr = valStr.split("").reverse().join(""); } } } else if (c.toLowerCase() == "l") { val = array[p - 1]; if ((c == "l") && (val < 0)) { val += 0x100000000; } if ((val > 0xffffffff) || (val < 0)) { throw ("'pack' error."); } else { valStr = String.fromCharCode(Math.floor(val / 0x1000000)) + String.fromCharCode(Math.floor((val % 0x1000000) / 0x10000)) + String.fromCharCode(Math.floor((val % 0x10000) / 0x100)) + String.fromCharCode(val % 0x100); if (littleEndian) { valStr = valStr.split("").reverse().join(""); } } } else { throw ("'pack' error."); } packed += valStr; p += 1; } return packed; } function unpack(mark, str) { if (typeof (str) != "string") { throw ("'unpack' error. Got invalid type argument."); } var l = 0; for (var markPointer = 1; markPointer < mark.length; markPointer++) { if (mark[markPointer].toLowerCase() == "b") { l += 1; } else if (mark[markPointer].toLowerCase() == "h") { l += 2; } else if (mark[markPointer].toLowerCase() == "l") { l += 4; } else { throw ("'unpack' error. Got invalid mark."); } } if (l != str.length) { throw ("'unpack' error. Mismatch between symbol and string length. " + l + ":" + str.length); } var littleEndian; if (mark[0] == "<") { littleEndian = true; } else if (mark[0] == ">") { littleEndian = false; } else { throw ("'unpack' error."); } var unpacked = []; var strPointer = 0; var p = 1; var val = null; var c = null; var length = null; var sliced = ""; while (c = mark[p]) { if (c.toLowerCase() == "b") { length = 1; sliced = str.slice(strPointer, strPointer + length); val = sliced.charCodeAt(0); if ((c == "b") && (val >= 0x80)) { val -= 0x100; } } else if (c == "H") { length = 2; sliced = str.slice(strPointer, strPointer + length); if (littleEndian) { sliced = sliced.split("").reverse().join(""); } val = sliced.charCodeAt(0) * 0x100 + sliced.charCodeAt(1); } else if (c.toLowerCase() == "l") { length = 4; sliced = str.slice(strPointer, strPointer + length); if (littleEndian) { sliced = sliced.split("").reverse().join(""); } val = sliced.charCodeAt(0) * 0x1000000 + sliced.charCodeAt(1) * 0x10000 + sliced.charCodeAt(2) * 0x100 + sliced.charCodeAt(3); if ((c == "l") && (val >= 0x80000000)) { val -= 0x100000000; } } else { throw ("'unpack' error. " + c); } unpacked.push(val); strPointer += length; p += 1; } return unpacked; } function nStr(ch, num) { var str = ""; for (var i = 0; i < num; i++) { str += ch; } return str; } function splitIntoSegments(data) { if (data.slice(0, 2) != "\xff\xd8") { throw ("Given data isn't JPEG."); } var head = 2; var segments = ["\xff\xd8"]; while (true) { if (data.slice(head, head + 2) == "\xff\xda") { segments.push(data.slice(head)); break; } else { var length = unpack(">H", data.slice(head + 2, head + 4))[0]; var endPoint = head + length + 2; segments.push(data.slice(head, endPoint)); head = endPoint; } if (head >= data.length) { throw ("Wrong JPEG data."); } } return segments; } function getExifSeg(segments) { var seg; for (var i = 0; i < segments.length; i++) { seg = segments[i]; if (seg.slice(0, 2) == "\xff\xe1" && seg.slice(4, 10) == "Exif\x00\x00") { return seg; } } return null; } function mergeSegments(segments, exif) { if (segments[1].slice(0, 2) == "\xff\xe0" && (segments[2].slice(0, 2) == "\xff\xe1" && segments[2].slice(4, 10) == "Exif\x00\x00")) { if (exif) { segments[2] = exif; segments = ["\xff\xd8"].concat(segments.slice(2)); } else if (exif == null) { segments = segments.slice(0, 2).concat(segments.slice(3)); } else { segments = segments.slice(0).concat(segments.slice(2)); } } else if (segments[1].slice(0, 2) == "\xff\xe0") { if (exif) { segments[1] = exif; } } else if (segments[1].slice(0, 2) == "\xff\xe1" && segments[1].slice(4, 10) == "Exif\x00\x00") { if (exif) { segments[1] = exif; } else if (exif == null) { segments = segments.slice(0).concat(segments.slice(2)); } } else { if (exif) { segments = [segments[0], exif].concat(segments.slice(1)); } } return segments.join(""); } function toHex(str) { var hexStr = ""; for (var i = 0; i < str.length; i++) { var h = str.charCodeAt(i); var hex = ((h < 10) ? "0" : "") + h.toString(16); hexStr += hex + " "; } return hexStr; } var TYPES = { "Byte": 1, "Ascii": 2, "Short": 3, "Long": 4, "Rational": 5, "Undefined": 7, "SLong": 9, "SRational": 10 }; var TAGS = { 'Image': { 11: { 'name': 'ProcessingSoftware', 'type': 'Ascii' }, 254: { 'name': 'NewSubfileType', 'type': 'Long' }, 255: { 'name': 'SubfileType', 'type': 'Short' }, 256: { 'name': 'ImageWidth', 'type': 'Long' }, 257: { 'name': 'ImageLength', 'type': 'Long' }, 258: { 'name': 'BitsPerSample', 'type': 'Short' }, 259: { 'name': 'Compression', 'type': 'Short' }, 262: { 'name': 'PhotometricInterpretation', 'type': 'Short' }, 263: { 'name': 'Threshholding', 'type': 'Short' }, 264: { 'name': 'CellWidth', 'type': 'Short' }, 265: { 'name': 'CellLength', 'type': 'Short' }, 266: { 'name': 'FillOrder', 'type': 'Short' }, 269: { 'name': 'DocumentName', 'type': 'Ascii' }, 270: { 'name': 'ImageDescription', 'type': 'Ascii' }, 271: { 'name': 'Make', 'type': 'Ascii' }, 272: { 'name': 'Model', 'type': 'Ascii' }, 273: { 'name': 'StripOffsets', 'type': 'Long' }, 274: { 'name': 'Orientation', 'type': 'Short' }, 277: { 'name': 'SamplesPerPixel', 'type': 'Short' }, 278: { 'name': 'RowsPerStrip', 'type': 'Long' }, 279: { 'name': 'StripByteCounts', 'type': 'Long' }, 282: { 'name': 'XResolution', 'type': 'Rational' }, 283: { 'name': 'YResolution', 'type': 'Rational' }, 284: { 'name': 'PlanarConfiguration', 'type': 'Short' }, 290: { 'name': 'GrayResponseUnit', 'type': 'Short' }, 291: { 'name': 'GrayResponseCurve', 'type': 'Short' }, 292: { 'name': 'T4Options', 'type': 'Long' }, 293: { 'name': 'T6Options', 'type': 'Long' }, 296: { 'name': 'ResolutionUnit', 'type': 'Short' }, 301: { 'name': 'TransferFunction', 'type': 'Short' }, 305: { 'name': 'Software', 'type': 'Ascii' }, 306: { 'name': 'DateTime', 'type': 'Ascii' }, 315: { 'name': 'Artist', 'type': 'Ascii' }, 316: { 'name': 'HostComputer', 'type': 'Ascii' }, 317: { 'name': 'Predictor', 'type': 'Short' }, 318: { 'name': 'WhitePoint', 'type': 'Rational' }, 319: { 'name': 'PrimaryChromaticities', 'type': 'Rational' }, 320: { 'name': 'ColorMap', 'type': 'Short' }, 321: { 'name': 'HalftoneHints', 'type': 'Short' }, 322: { 'name': 'TileWidth', 'type': 'Short' }, 323: { 'name': 'TileLength', 'type': 'Short' }, 324: { 'name': 'TileOffsets', 'type': 'Short' }, 325: { 'name': 'TileByteCounts', 'type': 'Short' }, 330: { 'name': 'SubIFDs', 'type': 'Long' }, 332: { 'name': 'InkSet', 'type': 'Short' }, 333: { 'name': 'InkNames', 'type': 'Ascii' }, 334: { 'name': 'NumberOfInks', 'type': 'Short' }, 336: { 'name': 'DotRange', 'type': 'Byte' }, 337: { 'name': 'TargetPrinter', 'type': 'Ascii' }, 338: { 'name': 'ExtraSamples', 'type': 'Short' }, 339: { 'name': 'SampleFormat', 'type': 'Short' }, 340: { 'name': 'SMinSampleValue', 'type': 'Short' }, 341: { 'name': 'SMaxSampleValue', 'type': 'Short' }, 342: { 'name': 'TransferRange', 'type': 'Short' }, 343: { 'name': 'ClipPath', 'type': 'Byte' }, 344: { 'name': 'XClipPathUnits', 'type': 'Long' }, 345: { 'name': 'YClipPathUnits', 'type': 'Long' }, 346: { 'name': 'Indexed', 'type': 'Short' }, 347: { 'name': 'JPEGTables', 'type': 'Undefined' }, 351: { 'name': 'OPIProxy', 'type': 'Short' }, 512: { 'name': 'JPEGProc', 'type': 'Long' }, 513: { 'name': 'JPEGInterchangeFormat', 'type': 'Long' }, 514: { 'name': 'JPEGInterchangeFormatLength', 'type': 'Long' }, 515: { 'name': 'JPEGRestartInterval', 'type': 'Short' }, 517: { 'name': 'JPEGLosslessPredictors', 'type': 'Short' }, 518: { 'name': 'JPEGPointTransforms', 'type': 'Short' }, 519: { 'name': 'JPEGQTables', 'type': 'Long' }, 520: { 'name': 'JPEGDCTables', 'type': 'Long' }, 521: { 'name': 'JPEGACTables', 'type': 'Long' }, 529: { 'name': 'YCbCrCoefficients', 'type': 'Rational' }, 530: { 'name': 'YCbCrSubSampling', 'type': 'Short' }, 531: { 'name': 'YCbCrPositioning', 'type': 'Short' }, 532: { 'name': 'ReferenceBlackWhite', 'type': 'Rational' }, 700: { 'name': 'XMLPacket', 'type': 'Byte' }, 18246: { 'name': 'Rating', 'type': 'Short' }, 18249: { 'name': 'RatingPercent', 'type': 'Short' }, 32781: { 'name': 'ImageID', 'type': 'Ascii' }, 33421: { 'name': 'CFARepeatPatternDim', 'type': 'Short' }, 33422: { 'name': 'CFAPattern', 'type': 'Byte' }, 33423: { 'name': 'BatteryLevel', 'type': 'Rational' }, 33432: { 'name': 'Copyright', 'type': 'Ascii' }, 33434: { 'name': 'ExposureTime', 'type': 'Rational' }, 34377: { 'name': 'ImageResources', 'type': 'Byte' }, 34665: { 'name': 'ExifTag', 'type': 'Long' }, 34675: { 'name': 'InterColorProfile', 'type': 'Undefined' }, 34853: { 'name': 'GPSTag', 'type': 'Long' }, 34857: { 'name': 'Interlace', 'type': 'Short' }, 34858: { 'name': 'TimeZoneOffset', 'type': 'Long' }, 34859: { 'name': 'SelfTimerMode', 'type': 'Short' }, 37387: { 'name': 'FlashEnergy', 'type': 'Rational' }, 37388: { 'name': 'SpatialFrequencyResponse', 'type': 'Undefined' }, 37389: { 'name': 'Noise', 'type': 'Undefined' }, 37390: { 'name': 'FocalPlaneXResolution', 'type': 'Rational' }, 37391: { 'name': 'FocalPlaneYResolution', 'type': 'Rational' }, 37392: { 'name': 'FocalPlaneResolutionUnit', 'type': 'Short' }, 37393: { 'name': 'ImageNumber', 'type': 'Long' }, 37394: { 'name': 'SecurityClassification', 'type': 'Ascii' }, 37395: { 'name': 'ImageHistory', 'type': 'Ascii' }, 37397: { 'name': 'ExposureIndex', 'type': 'Rational' }, 37398: { 'name': 'TIFFEPStandardID', 'type': 'Byte' }, 37399: { 'name': 'SensingMethod', 'type': 'Short' }, 40091: { 'name': 'XPTitle', 'type': 'Byte' }, 40092: { 'name': 'XPComment', 'type': 'Byte' }, 40093: { 'name': 'XPAuthor', 'type': 'Byte' }, 40094: { 'name': 'XPKeywords', 'type': 'Byte' }, 40095: { 'name': 'XPSubject', 'type': 'Byte' }, 50341: { 'name': 'PrintImageMatching', 'type': 'Undefined' }, 50706: { 'name': 'DNGVersion', 'type': 'Byte' }, 50707: { 'name': 'DNGBackwardVersion', 'type': 'Byte' }, 50708: { 'name': 'UniqueCameraModel', 'type': 'Ascii' }, 50709: { 'name': 'LocalizedCameraModel', 'type': 'Byte' }, 50710: { 'name': 'CFAPlaneColor', 'type': 'Byte' }, 50711: { 'name': 'CFALayout', 'type': 'Short' }, 50712: { 'name': 'LinearizationTable', 'type': 'Short' }, 50713: { 'name': 'BlackLevelRepeatDim', 'type': 'Short' }, 50714: { 'name': 'BlackLevel', 'type': 'Rational' }, 50715: { 'name': 'BlackLevelDeltaH', 'type': 'SRational' }, 50716: { 'name': 'BlackLevelDeltaV', 'type': 'SRational' }, 50717: { 'name': 'WhiteLevel', 'type': 'Short' }, 50718: { 'name': 'DefaultScale', 'type': 'Rational' }, 50719: { 'name': 'DefaultCropOrigin', 'type': 'Short' }, 50720: { 'name': 'DefaultCropSize', 'type': 'Short' }, 50721: { 'name': 'ColorMatrix1', 'type': 'SRational' }, 50722: { 'name': 'ColorMatrix2', 'type': 'SRational' }, 50723: { 'name': 'CameraCalibration1', 'type': 'SRational' }, 50724: { 'name': 'CameraCalibration2', 'type': 'SRational' }, 50725: { 'name': 'ReductionMatrix1', 'type': 'SRational' }, 50726: { 'name': 'ReductionMatrix2', 'type': 'SRational' }, 50727: { 'name': 'AnalogBalance', 'type': 'Rational' }, 50728: { 'name': 'AsShotNeutral', 'type': 'Short' }, 50729: { 'name': 'AsShotWhiteXY', 'type': 'Rational' }, 50730: { 'name': 'BaselineExposure', 'type': 'SRational' }, 50731: { 'name': 'BaselineNoise', 'type': 'Rational' }, 50732: { 'name': 'BaselineSharpness', 'type': 'Rational' }, 50733: { 'name': 'BayerGreenSplit', 'type': 'Long' }, 50734: { 'name': 'LinearResponseLimit', 'type': 'Rational' }, 50735: { 'name': 'CameraSerialNumber', 'type': 'Ascii' }, 50736: { 'name': 'LensInfo', 'type': 'Rational' }, 50737: { 'name': 'ChromaBlurRadius', 'type': 'Rational' }, 50738: { 'name': 'AntiAliasStrength', 'type': 'Rational' }, 50739: { 'name': 'ShadowScale', 'type': 'SRational' }, 50740: { 'name': 'DNGPrivateData', 'type': 'Byte' }, 50741: { 'name': 'MakerNoteSafety', 'type': 'Short' }, 50778: { 'name': 'CalibrationIlluminant1', 'type': 'Short' }, 50779: { 'name': 'CalibrationIlluminant2', 'type': 'Short' }, 50780: { 'name': 'BestQualityScale', 'type': 'Rational' }, 50781: { 'name': 'RawDataUniqueID', 'type': 'Byte' }, 50827: { 'name': 'OriginalRawFileName', 'type': 'Byte' }, 50828: { 'name': 'OriginalRawFileData', 'type': 'Undefined' }, 50829: { 'name': 'ActiveArea', 'type': 'Short' }, 50830: { 'name': 'MaskedAreas', 'type': 'Short' }, 50831: { 'name': 'AsShotICCProfile', 'type': 'Undefined' }, 50832: { 'name': 'AsShotPreProfileMatrix', 'type': 'SRational' }, 50833: { 'name': 'CurrentICCProfile', 'type': 'Undefined' }, 50834: { 'name': 'CurrentPreProfileMatrix', 'type': 'SRational' }, 50879: { 'name': 'ColorimetricReference', 'type': 'Short' }, 50931: { 'name': 'CameraCalibrationSignature', 'type': 'Byte' }, 50932: { 'name': 'ProfileCalibrationSignature', 'type': 'Byte' }, 50934: { 'name': 'AsShotProfileName', 'type': 'Byte' }, 50935: { 'name': 'NoiseReductionApplied', 'type': 'Rational' }, 50936: { 'name': 'ProfileName', 'type': 'Byte' }, 50937: { 'name': 'ProfileHueSatMapDims', 'type': 'Long' }, 50938: { 'name': 'ProfileHueSatMapData1', 'type': 'Float' }, 50939: { 'name': 'ProfileHueSatMapData2', 'type': 'Float' }, 50940: { 'name': 'ProfileToneCurve', 'type': 'Float' }, 50941: { 'name': 'ProfileEmbedPolicy', 'type': 'Long' }, 50942: { 'name': 'ProfileCopyright', 'type': 'Byte' }, 50964: { 'name': 'ForwardMatrix1', 'type': 'SRational' }, 50965: { 'name': 'ForwardMatrix2', 'type': 'SRational' }, 50966: { 'name': 'PreviewApplicationName', 'type': 'Byte' }, 50967: { 'name': 'PreviewApplicationVersion', 'type': 'Byte' }, 50968: { 'name': 'PreviewSettingsName', 'type': 'Byte' }, 50969: { 'name': 'PreviewSettingsDigest', 'type': 'Byte' }, 50970: { 'name': 'PreviewColorSpace', 'type': 'Long' }, 50971: { 'name': 'PreviewDateTime', 'type': 'Ascii' }, 50972: { 'name': 'RawImageDigest', 'type': 'Undefined' }, 50973: { 'name': 'OriginalRawFileDigest', 'type': 'Undefined' }, 50974: { 'name': 'SubTileBlockSize', 'type': 'Long' }, 50975: { 'name': 'RowInterleaveFactor', 'type': 'Long' }, 50981: { 'name': 'ProfileLookTableDims', 'type': 'Long' }, 50982: { 'name': 'ProfileLookTableData', 'type': 'Float' }, 51008: { 'name': 'OpcodeList1', 'type': 'Undefined' }, 51009: { 'name': 'OpcodeList2', 'type': 'Undefined' }, 51022: { 'name': 'OpcodeList3', 'type': 'Undefined' } }, 'Exif': { 33434: { 'name': 'ExposureTime', 'type': 'Rational' }, 33437: { 'name': 'FNumber', 'type': 'Rational' }, 34850: { 'name': 'ExposureProgram', 'type': 'Short' }, 34852: { 'name': 'SpectralSensitivity', 'type': 'Ascii' }, 34855: { 'name': 'ISOSpeedRatings', 'type': 'Short' }, 34856: { 'name': 'OECF', 'type': 'Undefined' }, 34864: { 'name': 'SensitivityType', 'type': 'Short' }, 34865: { 'name': 'StandardOutputSensitivity', 'type': 'Long' }, 34866: { 'name': 'RecommendedExposureIndex', 'type': 'Long' }, 34867: { 'name': 'ISOSpeed', 'type': 'Long' }, 34868: { 'name': 'ISOSpeedLatitudeyyy', 'type': 'Long' }, 34869: { 'name': 'ISOSpeedLatitudezzz', 'type': 'Long' }, 36864: { 'name': 'ExifVersion', 'type': 'Undefined' }, 36867: { 'name': 'DateTimeOriginal', 'type': 'Ascii' }, 36868: { 'name': 'DateTimeDigitized', 'type': 'Ascii' }, 37121: { 'name': 'ComponentsConfiguration', 'type': 'Undefined' }, 37122: { 'name': 'CompressedBitsPerPixel', 'type': 'Rational' }, 37377: { 'name': 'ShutterSpeedValue', 'type': 'SRational' }, 37378: { 'name': 'ApertureValue', 'type': 'Rational' }, 37379: { 'name': 'BrightnessValue', 'type': 'SRational' }, 37380: { 'name': 'ExposureBiasValue', 'type': 'SRational' }, 37381: { 'name': 'MaxApertureValue', 'type': 'Rational' }, 37382: { 'name': 'SubjectDistance', 'type': 'Rational' }, 37383: { 'name': 'MeteringMode', 'type': 'Short' }, 37384: { 'name': 'LightSource', 'type': 'Short' }, 37385: { 'name': 'Flash', 'type': 'Short' }, 37386: { 'name': 'FocalLength', 'type': 'Rational' }, 37396: { 'name': 'SubjectArea', 'type': 'Short' }, 37500: { 'name': 'MakerNote', 'type': 'Undefined' }, 37510: { 'name': 'UserComment', 'type': 'Ascii' }, 37520: { 'name': 'SubSecTime', 'type': 'Ascii' }, 37521: { 'name': 'SubSecTimeOriginal', 'type': 'Ascii' }, 37522: { 'name': 'SubSecTimeDigitized', 'type': 'Ascii' }, 40960: { 'name': 'FlashpixVersion', 'type': 'Undefined' }, 40961: { 'name': 'ColorSpace', 'type': 'Short' }, 40962: { 'name': 'PixelXDimension', 'type': 'Long' }, 40963: { 'name': 'PixelYDimension', 'type': 'Long' }, 40964: { 'name': 'RelatedSoundFile', 'type': 'Ascii' }, 40965: { 'name': 'InteroperabilityTag', 'type': 'Long' }, 41483: { 'name': 'FlashEnergy', 'type': 'Rational' }, 41484: { 'name': 'SpatialFrequencyResponse', 'type': 'Undefined' }, 41486: { 'name': 'FocalPlaneXResolution', 'type': 'Rational' }, 41487: { 'name': 'FocalPlaneYResolution', 'type': 'Rational' }, 41488: { 'name': 'FocalPlaneResolutionUnit', 'type': 'Short' }, 41492: { 'name': 'SubjectLocation', 'type': 'Short' }, 41493: { 'name': 'ExposureIndex', 'type': 'Rational' }, 41495: { 'name': 'SensingMethod', 'type': 'Short' }, 41728: { 'name': 'FileSource', 'type': 'Undefined' }, 41729: { 'name': 'SceneType', 'type': 'Undefined' }, 41730: { 'name': 'CFAPattern', 'type': 'Undefined' }, 41985: { 'name': 'CustomRendered', 'type': 'Short' }, 41986: { 'name': 'ExposureMode', 'type': 'Short' }, 41987: { 'name': 'WhiteBalance', 'type': 'Short' }, 41988: { 'name': 'DigitalZoomRatio', 'type': 'Rational' }, 41989: { 'name': 'FocalLengthIn35mmFilm', 'type': 'Short' }, 41990: { 'name': 'SceneCaptureType', 'type': 'Short' }, 41991: { 'name': 'GainControl', 'type': 'Short' }, 41992: { 'name': 'Contrast', 'type': 'Short' }, 41993: { 'name': 'Saturation', 'type': 'Short' }, 41994: { 'name': 'Sharpness', 'type': 'Short' }, 41995: { 'name': 'DeviceSettingDescription', 'type': 'Undefined' }, 41996: { 'name': 'SubjectDistanceRange', 'type': 'Short' }, 42016: { 'name': 'ImageUniqueID', 'type': 'Ascii' }, 42032: { 'name': 'CameraOwnerName', 'type': 'Ascii' }, 42033: { 'name': 'BodySerialNumber', 'type': 'Ascii' }, 42034: { 'name': 'LensSpecification', 'type': 'Rational' }, 42035: { 'name': 'LensMake', 'type': 'Ascii' }, 42036: { 'name': 'LensModel', 'type': 'Ascii' }, 42037: { 'name': 'LensSerialNumber', 'type': 'Ascii' }, 42240: { 'name': 'Gamma', 'type': 'Rational' } }, 'GPS': { 0: { 'name': 'GPSVersionID', 'type': 'Byte' }, 1: { 'name': 'GPSLatitudeRef', 'type': 'Ascii' }, 2: { 'name': 'GPSLatitude', 'type': 'Rational' }, 3: { 'name': 'GPSLongitudeRef', 'type': 'Ascii' }, 4: { 'name': 'GPSLongitude', 'type': 'Rational' }, 5: { 'name': 'GPSAltitudeRef', 'type': 'Byte' }, 6: { 'name': 'GPSAltitude', 'type': 'Rational' }, 7: { 'name': 'GPSTimeStamp', 'type': 'Rational' }, 8: { 'name': 'GPSSatellites', 'type': 'Ascii' }, 9: { 'name': 'GPSStatus', 'type': 'Ascii' }, 10: { 'name': 'GPSMeasureMode', 'type': 'Ascii' }, 11: { 'name': 'GPSDOP', 'type': 'Rational' }, 12: { 'name': 'GPSSpeedRef', 'type': 'Ascii' }, 13: { 'name': 'GPSSpeed', 'type': 'Rational' }, 14: { 'name': 'GPSTrackRef', 'type': 'Ascii' }, 15: { 'name': 'GPSTrack', 'type': 'Rational' }, 16: { 'name': 'GPSImgDirectionRef', 'type': 'Ascii' }, 17: { 'name': 'GPSImgDirection', 'type': 'Rational' }, 18: { 'name': 'GPSMapDatum', 'type': 'Ascii' }, 19: { 'name': 'GPSDestLatitudeRef', 'type': 'Ascii' }, 20: { 'name': 'GPSDestLatitude', 'type': 'Rational' }, 21: { 'name': 'GPSDestLongitudeRef', 'type': 'Ascii' }, 22: { 'name': 'GPSDestLongitude', 'type': 'Rational' }, 23: { 'name': 'GPSDestBearingRef', 'type': 'Ascii' }, 24: { 'name': 'GPSDestBearing', 'type': 'Rational' }, 25: { 'name': 'GPSDestDistanceRef', 'type': 'Ascii' }, 26: { 'name': 'GPSDestDistance', 'type': 'Rational' }, 27: { 'name': 'GPSProcessingMethod', 'type': 'Undefined' }, 28: { 'name': 'GPSAreaInformation', 'type': 'Undefined' }, 29: { 'name': 'GPSDateStamp', 'type': 'Ascii' }, 30: { 'name': 'GPSDifferential', 'type': 'Short' }, 31: { 'name': 'GPSHPositioningError', 'type': 'Rational' } }, 'Interop': { 1: { 'name': 'InteroperabilityIndex', 'type': 'Ascii' } }, }; TAGS["0th"] = TAGS["Image"]; TAGS["1st"] = TAGS["Image"]; that.TAGS = TAGS; that.ImageIFD = { ProcessingSoftware:11, NewSubfileType:254, SubfileType:255, ImageWidth:256, ImageLength:257, BitsPerSample:258, Compression:259, PhotometricInterpretation:262, Threshholding:263, CellWidth:264, CellLength:265, FillOrder:266, DocumentName:269, ImageDescription:270, Make:271, Model:272, StripOffsets:273, Orientation:274, SamplesPerPixel:277, RowsPerStrip:278, StripByteCounts:279, XResolution:282, YResolution:283, PlanarConfiguration:284, GrayResponseUnit:290, GrayResponseCurve:291, T4Options:292, T6Options:293, ResolutionUnit:296, TransferFunction:301, Software:305, DateTime:306, Artist:315, HostComputer:316, Predictor:317, WhitePoint:318, PrimaryChromaticities:319, ColorMap:320, HalftoneHints:321, TileWidth:322, TileLength:323, TileOffsets:324, TileByteCounts:325, SubIFDs:330, InkSet:332, InkNames:333, NumberOfInks:334, DotRange:336, TargetPrinter:337, ExtraSamples:338, SampleFormat:339, SMinSampleValue:340, SMaxSampleValue:341, TransferRange:342, ClipPath:343, XClipPathUnits:344, YClipPathUnits:345, Indexed:346, JPEGTables:347, OPIProxy:351, JPEGProc:512, JPEGInterchangeFormat:513, JPEGInterchangeFormatLength:514, JPEGRestartInterval:515, JPEGLosslessPredictors:517, JPEGPointTransforms:518, JPEGQTables:519, JPEGDCTables:520, JPEGACTables:521, YCbCrCoefficients:529, YCbCrSubSampling:530, YCbCrPositioning:531, ReferenceBlackWhite:532, XMLPacket:700, Rating:18246, RatingPercent:18249, ImageID:32781, CFARepeatPatternDim:33421, CFAPattern:33422, BatteryLevel:33423, Copyright:33432, ExposureTime:33434, ImageResources:34377, ExifTag:34665, InterColorProfile:34675, GPSTag:34853, Interlace:34857, TimeZoneOffset:34858, SelfTimerMode:34859, FlashEnergy:37387, SpatialFrequencyResponse:37388, Noise:37389, FocalPlaneXResolution:37390, FocalPlaneYResolution:37391, FocalPlaneResolutionUnit:37392, ImageNumber:37393, SecurityClassification:37394, ImageHistory:37395, ExposureIndex:37397, TIFFEPStandardID:37398, SensingMethod:37399, XPTitle:40091, XPComment:40092, XPAuthor:40093, XPKeywords:40094, XPSubject:40095, PrintImageMatching:50341, DNGVersion:50706, DNGBackwardVersion:50707, UniqueCameraModel:50708, LocalizedCameraModel:50709, CFAPlaneColor:50710, CFALayout:50711, LinearizationTable:50712, BlackLevelRepeatDim:50713, BlackLevel:50714, BlackLevelDeltaH:50715, BlackLevelDeltaV:50716, WhiteLevel:50717, DefaultScale:50718, DefaultCropOrigin:50719, DefaultCropSize:50720, ColorMatrix1:50721, ColorMatrix2:50722, CameraCalibration1:50723, CameraCalibration2:50724, ReductionMatrix1:50725, ReductionMatrix2:50726, AnalogBalance:50727, AsShotNeutral:50728, AsShotWhiteXY:50729, BaselineExposure:50730, BaselineNoise:50731, BaselineSharpness:50732, BayerGreenSplit:50733, LinearResponseLimit:50734, CameraSerialNumber:50735, LensInfo:50736, ChromaBlurRadius:50737, AntiAliasStrength:50738, ShadowScale:50739, DNGPrivateData:50740, MakerNoteSafety:50741, CalibrationIlluminant1:50778, CalibrationIlluminant2:50779, BestQualityScale:50780, RawDataUniqueID:50781, OriginalRawFileName:50827, OriginalRawFileData:50828, ActiveArea:50829, MaskedAreas:50830, AsShotICCProfile:50831, AsShotPreProfileMatrix:50832, CurrentICCProfile:50833, CurrentPreProfileMatrix:50834, ColorimetricReference:50879, CameraCalibrationSignature:50931, ProfileCalibrationSignature:50932, AsShotProfileName:50934, NoiseReductionApplied:50935, ProfileName:50936, ProfileHueSatMapDims:50937, ProfileHueSatMapData1:50938, ProfileHueSatMapData2:50939, ProfileToneCurve:50940, ProfileEmbedPolicy:50941, ProfileCopyright:50942, ForwardMatrix1:50964, ForwardMatrix2:50965, PreviewApplicationName:50966, PreviewApplicationVersion:50967, PreviewSettingsName:50968, PreviewSettingsDigest:50969, PreviewColorSpace:50970, PreviewDateTime:50971, RawImageDigest:50972, OriginalRawFileDigest:50973, SubTileBlockSize:50974, RowInterleaveFactor:50975, ProfileLookTableDims:50981, ProfileLookTableData:50982, OpcodeList1:51008, OpcodeList2:51009, OpcodeList3:51022, NoiseProfile:51041, }; that.ExifIFD = { ExposureTime:33434, FNumber:33437, ExposureProgram:34850, SpectralSensitivity:34852, ISOSpeedRatings:34855, OECF:34856, SensitivityType:34864, StandardOutputSensitivity:34865, RecommendedExposureIndex:34866, ISOSpeed:34867, ISOSpeedLatitudeyyy:34868, ISOSpeedLatitudezzz:34869, ExifVersion:36864, DateTimeOriginal:36867, DateTimeDigitized:36868, ComponentsConfiguration:37121, CompressedBitsPerPixel:37122, ShutterSpeedValue:37377, ApertureValue:37378, BrightnessValue:37379, ExposureBiasValue:37380, MaxApertureValue:37381, SubjectDistance:37382, MeteringMode:37383, LightSource:37384, Flash:37385, FocalLength:37386, SubjectArea:37396, MakerNote:37500, UserComment:37510, SubSecTime:37520, SubSecTimeOriginal:37521, SubSecTimeDigitized:37522, FlashpixVersion:40960, ColorSpace:40961, PixelXDimension:40962, PixelYDimension:40963, RelatedSoundFile:40964, InteroperabilityTag:40965, FlashEnergy:41483, SpatialFrequencyResponse:41484, FocalPlaneXResolution:41486, FocalPlaneYResolution:41487, FocalPlaneResolutionUnit:41488, SubjectLocation:41492, ExposureIndex:41493, SensingMethod:41495, FileSource:41728, SceneType:41729, CFAPattern:41730, CustomRendered:41985, ExposureMode:41986, WhiteBalance:41987, DigitalZoomRatio:41988, FocalLengthIn35mmFilm:41989, SceneCaptureType:41990, GainControl:41991, Contrast:41992, Saturation:41993, Sharpness:41994, DeviceSettingDescription:41995, SubjectDistanceRange:41996, ImageUniqueID:42016, CameraOwnerName:42032, BodySerialNumber:42033, LensSpecification:42034, LensMake:42035, LensModel:42036, LensSerialNumber:42037, Gamma:42240, }; that.GPSIFD = { GPSVersionID:0, GPSLatitudeRef:1, GPSLatitude:2, GPSLongitudeRef:3, GPSLongitude:4, GPSAltitudeRef:5, GPSAltitude:6, GPSTimeStamp:7, GPSSatellites:8, GPSStatus:9, GPSMeasureMode:10, GPSDOP:11, GPSSpeedRef:12, GPSSpeed:13, GPSTrackRef:14, GPSTrack:15, GPSImgDirectionRef:16, GPSImgDirection:17, GPSMapDatum:18, GPSDestLatitudeRef:19, GPSDestLatitude:20, GPSDestLongitudeRef:21, GPSDestLongitude:22, GPSDestBearingRef:23, GPSDestBearing:24, GPSDestDistanceRef:25, GPSDestDistance:26, GPSProcessingMethod:27, GPSAreaInformation:28, GPSDateStamp:29, GPSDifferential:30, GPSHPositioningError:31, }; that.InteropIFD = { InteroperabilityIndex:1, }; that.GPSHelper = { degToDmsRational:function (degFloat) { var minFloat = degFloat % 1 * 60; var secFloat = minFloat % 1 * 60; var deg = Math.floor(degFloat); var min = Math.floor(minFloat); var sec = Math.round(secFloat * 100); return [[deg, 1], [min, 1], [sec, 100]]; } }; if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = that; } exports.piexif = that; } else { window.piexif = that; } })(); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/plugins/purify.js ================================================ ;(function(factory) { 'use strict'; /* global window: false, define: false, module: false */ var root = typeof window === 'undefined' ? null : window; if (typeof define === 'function' && define.amd) { define(function(){ return factory(root); }); } else if (typeof module !== 'undefined') { module.exports = factory(root); } else { root.DOMPurify = factory(root); } }(function factory(window) { 'use strict'; var DOMPurify = function(window) { return factory(window); }; /** * Version label, exposed for easier checks * if DOMPurify is up to date or not */ DOMPurify.version = '0.7.4'; if (!window || !window.document || window.document.nodeType !== 9) { // not running in a browser, provide a factory function // so that you can pass your own Window DOMPurify.isSupported = false; return DOMPurify; } var document = window.document; var originalDocument = document; var DocumentFragment = window.DocumentFragment; var HTMLTemplateElement = window.HTMLTemplateElement; var NodeFilter = window.NodeFilter; var NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap; var Text = window.Text; var Comment = window.Comment; var DOMParser = window.DOMParser; // As per issue #47, the web-components registry is inherited by a // new document created via createHTMLDocument. As per the spec // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) // a new empty registry is used when creating a template contents owner // document, so we use that as our parent document to ensure nothing // is inherited. if (typeof HTMLTemplateElement === 'function') { var template = document.createElement('template'); if (template.content && template.content.ownerDocument) { document = template.content.ownerDocument; } } var implementation = document.implementation; var createNodeIterator = document.createNodeIterator; var getElementsByTagName = document.getElementsByTagName; var createDocumentFragment = document.createDocumentFragment; var importNode = originalDocument.importNode; var hooks = {}; /** * Expose whether this browser supports running the full DOMPurify. */ DOMPurify.isSupported = typeof implementation.createHTMLDocument !== 'undefined' && document.documentMode !== 9; /* Add properties to a lookup table */ var _addToSet = function(set, array) { var l = array.length; while (l--) { if (typeof array[l] === 'string') { array[l] = array[l].toLowerCase(); } set[array[l]] = true; } return set; }; /* Shallow clone an object */ var _cloneObj = function(object) { var newObject = {}; var property; for (property in object) { if (object.hasOwnProperty(property)) { newObject[property] = object[property]; } } return newObject; }; /** * We consider the elements and attributes below to be safe. Ideally * don't add any new ones but feel free to remove unwanted ones. */ /* allowed element names */ var ALLOWED_TAGS = null; var DEFAULT_ALLOWED_TAGS = _addToSet({}, [ // HTML 'a','abbr','acronym','address','area','article','aside','audio','b', 'bdi','bdo','big','blink','blockquote','body','br','button','canvas', 'caption','center','cite','code','col','colgroup','content','data', 'datalist','dd','decorator','del','details','dfn','dir','div','dl','dt', 'element','em','fieldset','figcaption','figure','font','footer','form', 'h1','h2','h3','h4','h5','h6','head','header','hgroup','hr','html','i', 'img','input','ins','kbd','label','legend','li','main','map','mark', 'marquee','menu','menuitem','meter','nav','nobr','ol','optgroup', 'option','output','p','pre','progress','q','rp','rt','ruby','s','samp', 'section','select','shadow','small','source','spacer','span','strike', 'strong','style','sub','summary','sup','table','tbody','td','template', 'textarea','tfoot','th','thead','time','tr','track','tt','u','ul','var', 'video','wbr', // SVG 'svg','altglyph','altglyphdef','altglyphitem','animatecolor', 'animatemotion','animatetransform','circle','clippath','defs','desc', 'ellipse','filter','font','g','glyph','glyphref','hkern','image','line', 'lineargradient','marker','mask','metadata','mpath','path','pattern', 'polygon','polyline','radialgradient','rect','stop','switch','symbol', 'text','textpath','title','tref','tspan','view','vkern', // SVG Filters 'feBlend','feColorMatrix','feComponentTransfer','feComposite', 'feConvolveMatrix','feDiffuseLighting','feDisplacementMap', 'feFlood','feFuncA','feFuncB','feFuncG','feFuncR','feGaussianBlur', 'feMerge','feMergeNode','feMorphology','feOffset', 'feSpecularLighting','feTile','feTurbulence', //MathML 'math','menclose','merror','mfenced','mfrac','mglyph','mi','mlabeledtr', 'mmuliscripts','mn','mo','mover','mpadded','mphantom','mroot','mrow', 'ms','mpspace','msqrt','mystyle','msub','msup','msubsup','mtable','mtd', 'mtext','mtr','munder','munderover', //Text '#text' ]); /* Allowed attribute names */ var ALLOWED_ATTR = null; var DEFAULT_ALLOWED_ATTR = _addToSet({}, [ // HTML 'accept','action','align','alt','autocomplete','background','bgcolor', 'border','cellpadding','cellspacing','checked','cite','class','clear','color', 'cols','colspan','coords','datetime','default','dir','disabled', 'download','enctype','face','for','headers','height','hidden','high','href', 'hreflang','id','ismap','label','lang','list','loop', 'low','max', 'maxlength','media','method','min','multiple','name','noshade','novalidate', 'nowrap','open','optimum','pattern','placeholder','poster','preload','pubdate', 'radiogroup','readonly','rel','required','rev','reversed','rows', 'rowspan','spellcheck','scope','selected','shape','size','span', 'srclang','start','src','step','style','summary','tabindex','title', 'type','usemap','valign','value','width','xmlns', // SVG 'accent-height','accumulate','additivive','alignment-baseline', 'ascent','attributename','attributetype','azimuth','basefrequency', 'baseline-shift','begin','bias','by','clip','clip-path','clip-rule', 'color','color-interpolation','color-interpolation-filters','color-profile', 'color-rendering','cx','cy','d','dx','dy','diffuseconstant','direction', 'display','divisor','dur','edgemode','elevation','end','fill','fill-opacity', 'fill-rule','filter','flood-color','flood-opacity','font-family','font-size', 'font-size-adjust','font-stretch','font-style','font-variant','font-weight', 'fx', 'fy','g1','g2','glyph-name','glyphref','gradientunits','gradienttransform', 'image-rendering','in','in2','k','k1','k2','k3','k4','kerning','keypoints', 'keysplines','keytimes','lengthadjust','letter-spacing','kernelmatrix', 'kernelunitlength','lighting-color','local','marker-end','marker-mid', 'marker-start','markerheight','markerunits','markerwidth','maskcontentunits', 'maskunits','max','mask','mode','min','numoctaves','offset','operator', 'opacity','order','orient','orientation','origin','overflow','paint-order', 'path','pathlength','patterncontentunits','patterntransform','patternunits', 'points','preservealpha','r','rx','ry','radius','refx','refy','repeatcount', 'repeatdur','restart','result','rotate','scale','seed','shape-rendering', 'specularconstant','specularexponent','spreadmethod','stddeviation','stitchtiles', 'stop-color','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap', 'stroke-linejoin','stroke-miterlimit','stroke-opacity','stroke','stroke-width', 'surfacescale','targetx','targety','transform','text-anchor','text-decoration', 'text-rendering','textlength','u1','u2','unicode','values','viewbox', 'visibility','vert-adv-y','vert-origin-x','vert-origin-y','word-spacing', 'wrap','writing-mode','xchannelselector','ychannelselector','x','x1','x2', 'y','y1','y2','z','zoomandpan', // MathML 'accent','accentunder','bevelled','close','columnsalign','columnlines', 'columnspan','denomalign','depth','display','displaystyle','fence', 'frame','largeop','length','linethickness','lspace','lquote', 'mathbackground','mathcolor','mathsize','mathvariant','maxsize', 'minsize','movablelimits','notation','numalign','open','rowalign', 'rowlines','rowspacing','rowspan','rspace','rquote','scriptlevel', 'scriptminsize','scriptsizemultiplier','selection','separator', 'separators','stretchy','subscriptshift','supscriptshift','symmetric', 'voffset', // XML 'xlink:href','xml:id','xlink:title','xml:space','xmlns:xlink' ]); /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ var FORBID_TAGS = null; /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ var FORBID_ATTR = null; /* Decide if custom data attributes are okay */ var ALLOW_DATA_ATTR = true; /* Decide if unknown protocols are okay */ var ALLOW_UNKNOWN_PROTOCOLS = false; /* Output should be safe for jQuery's $() factory? */ var SAFE_FOR_JQUERY = false; /* Output should be safe for common template engines. * This means, DOMPurify removes data attributes, mustaches and ERB */ var SAFE_FOR_TEMPLATES = false; /* Specify template detection regex for SAFE_FOR_TEMPLATES mode */ var MUSTACHE_EXPR = /\{\{[\s\S]*|[\s\S]*\}\}/gm; var ERB_EXPR = /<%[\s\S]*|[\s\S]*%>/gm; /* Decide if document with ... should be returned */ var WHOLE_DOCUMENT = false; /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html string. * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead */ var RETURN_DOM = false; /* Decide if a DOM `DocumentFragment` should be returned, instead of a html string */ var RETURN_DOM_FRAGMENT = false; /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM * `Node` is imported into the current `Document`. If this flag is not enabled the * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by * DOMPurify. */ var RETURN_DOM_IMPORT = false; /* Output should be free from DOM clobbering attacks? */ var SANITIZE_DOM = true; /* Keep element content when removing element? */ var KEEP_CONTENT = true; /* Tags to ignore content of when KEEP_CONTENT is true */ var FORBID_CONTENTS = _addToSet({}, [ 'audio', 'head', 'math', 'script', 'style', 'svg', 'video' ]); /* Tags that are safe for data: URIs */ var DATA_URI_TAGS = _addToSet({}, [ 'audio', 'video', 'img', 'source' ]); /* Attributes safe for values like "javascript:" */ var URI_SAFE_ATTRIBUTES = _addToSet({}, [ 'alt','class','for','id','label','name','pattern','placeholder', 'summary','title','value','style','xmlns' ]); /* Keep a reference to config to pass to hooks */ var CONFIG = null; /* Ideally, do not touch anything below this line */ /* ______________________________________________ */ var formElement = document.createElement('form'); /** * _parseConfig * * @param optional config literal */ var _parseConfig = function(cfg) { /* Shield configuration object from tampering */ if (typeof cfg !== 'object') { cfg = {}; } /* Set configuration parameters */ ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? _addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? _addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; FORBID_TAGS = 'FORBID_TAGS' in cfg ? _addToSet({}, cfg.FORBID_TAGS) : {}; FORBID_ATTR = 'FORBID_ATTR' in cfg ? _addToSet({}, cfg.FORBID_ATTR) : {}; ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY || false; // Default false SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false RETURN_DOM = cfg.RETURN_DOM || false; // Default false RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT || false; // Default false SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true if (SAFE_FOR_TEMPLATES) { ALLOW_DATA_ATTR = false; } if (RETURN_DOM_FRAGMENT) { RETURN_DOM = true; } /* Merge configuration parameters */ if (cfg.ADD_TAGS) { if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS = _cloneObj(ALLOWED_TAGS); } _addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); } if (cfg.ADD_ATTR) { if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { ALLOWED_ATTR = _cloneObj(ALLOWED_ATTR); } _addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); } /* Add #text in case KEEP_CONTENT is set to true */ if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; } // Prevent further manipulation of configuration. // Not available in IE8, Safari 5, etc. if (Object && 'freeze' in Object) { Object.freeze(cfg); } CONFIG = cfg; }; /** * _forceRemove * * @param a DOM node */ var _forceRemove = function(node) { try { node.parentNode.removeChild(node); } catch (e) { node.outerHTML = ''; } }; /** * _initDocument * * @param a string of dirty markup * @return a DOM, filled with the dirty markup */ var _initDocument = function(dirty) { /* Create a HTML document using DOMParser */ var doc, body; try { doc = new DOMParser().parseFromString(dirty, 'text/html'); } catch (e) {} /* Some browsers throw, some browsers return null for the code above DOMParser with text/html support is only in very recent browsers. */ if (!doc) { doc = implementation.createHTMLDocument(''); body = doc.body; body.parentNode.removeChild(body.parentNode.firstElementChild); body.outerHTML = dirty; } /* Work on whole document or just its body */ if (typeof doc.getElementsByTagName === 'function') { return doc.getElementsByTagName( WHOLE_DOCUMENT ? 'html' : 'body')[0]; } return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; }; /** * _createIterator * * @param document/fragment to create iterator for * @return iterator instance */ var _createIterator = function(root) { return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function() { return NodeFilter.FILTER_ACCEPT; }, false ); }; /** * _isClobbered * * @param element to check for clobbering attacks * @return true if clobbered, false if safe */ var _isClobbered = function(elm) { if (elm instanceof Text || elm instanceof Comment) { return false; } if ( typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' ) { return true; } return false; }; /** * _sanitizeElements * * @protect nodeName * @protect textContent * @protect removeChild * * @param node to check for permission to exist * @return true if node was killed, false if left alive */ var _sanitizeElements = function(currentNode) { var tagName, content; /* Execute a hook if present */ _executeHook('beforeSanitizeElements', currentNode, null); /* Check if element is clobbered or can clobber */ if (_isClobbered(currentNode)) { _forceRemove(currentNode); return true; } /* Now let's check the element's type and name */ tagName = currentNode.nodeName.toLowerCase(); /* Execute a hook if present */ _executeHook('uponSanitizeElement', currentNode, { tagName: tagName }); /* Remove element if anything forbids its presence */ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { /* Keep content except for black-listed elements */ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && typeof currentNode.insertAdjacentHTML === 'function') { try { currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML); } catch (e) {} } _forceRemove(currentNode); return true; } /* Convert markup to cover jQuery behavior */ if (SAFE_FOR_JQUERY && !currentNode.firstElementChild && (!currentNode.content || !currentNode.content.firstElementChild)) { currentNode.innerHTML = currentNode.textContent.replace(/ tag that has an "id" // attribute at the time. if (lcName === 'name' && currentNode.nodeName === 'IMG' && attributes.id) { idAttr = attributes.id; attributes = Array.prototype.slice.apply(attributes); currentNode.removeAttribute('id'); currentNode.removeAttribute(name); if (attributes.indexOf(idAttr) > l) { currentNode.setAttribute('id', idAttr.value); } } else { // This avoids a crash in Safari v9.0 with double-ids. // The trick is to first set the id to be empty and then to // remove the attriubute if (name === 'id') { currentNode.setAttribute(name, ''); } currentNode.removeAttribute(name); } /* Did the hooks approve of the attribute? */ if (!hookEvent.keepAttr) { continue; } /* Make sure attribute cannot clobber */ if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in window || value in document || value in formElement)) { continue; } /* Sanitize attribute content to be template-safe */ if (SAFE_FOR_TEMPLATES) { value = value.replace(MUSTACHE_EXPR, ' '); value = value.replace(ERB_EXPR, ' '); } if ( /* Check the name is permitted */ (ALLOWED_ATTR[lcName] && !FORBID_ATTR[lcName] && ( /* Check no script, data or unknown possibly unsafe URI unless we know URI values are safe for that attribute */ URI_SAFE_ATTRIBUTES[lcName] || IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE,'')) || /* Keep image data URIs alive if src is allowed */ (lcName === 'src' && value.indexOf('data:') === 0 && DATA_URI_TAGS[currentNode.nodeName.toLowerCase()]) )) || /* Allow potentially valid data-* attributes: * At least one character after "-" (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) * XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) * We don't need to check the value; it's always URI safe. */ (ALLOW_DATA_ATTR && DATA_ATTR.test(lcName)) || /* Allow unknown protocols: * This provides support for links that are handled by protocol handlers which may be unknown * ahead of time, e.g. fb:, spotify: */ (ALLOW_UNKNOWN_PROTOCOLS && !IS_SCRIPT_OR_DATA.test(value.replace(ATTR_WHITESPACE,''))) ) { /* Handle invalid data-* attribute set by try-catching it */ try { currentNode.setAttribute(name, value); } catch (e) {} } } /* Execute a hook if present */ _executeHook('afterSanitizeAttributes', currentNode, null); }; /** * _sanitizeShadowDOM * * @param fragment to iterate over recursively * @return void */ var _sanitizeShadowDOM = function(fragment) { var shadowNode; var shadowIterator = _createIterator(fragment); /* Execute a hook if present */ _executeHook('beforeSanitizeShadowDOM', fragment, null); while ( (shadowNode = shadowIterator.nextNode()) ) { /* Execute a hook if present */ _executeHook('uponSanitizeShadowNode', shadowNode, null); /* Sanitize tags and elements */ if (_sanitizeElements(shadowNode)) { continue; } /* Deep shadow DOM detected */ if (shadowNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(shadowNode.content); } /* Check attributes, sanitize if necessary */ _sanitizeAttributes(shadowNode); } /* Execute a hook if present */ _executeHook('afterSanitizeShadowDOM', fragment, null); }; /** * _executeHook * Execute user configurable hooks * * @param {String} entryPoint Name of the hook's entry point * @param {Node} currentNode */ var _executeHook = function(entryPoint, currentNode, data) { if (!hooks[entryPoint]) { return; } hooks[entryPoint].forEach(function(hook) { hook.call(DOMPurify, currentNode, data, CONFIG); }); }; /** * sanitize * Public method providing core sanitation functionality * * @param {String} dirty string * @param {Object} configuration object */ DOMPurify.sanitize = function(dirty, cfg) { var body, currentNode, oldNode, nodeIterator, returnNode; /* Make sure we have a string to sanitize. DO NOT return early, as this will return the wrong type if the user has requested a DOM object rather than a string */ if (!dirty) { dirty = ''; } /* Stringify, in case dirty is an object */ if (typeof dirty !== 'string') { if (typeof dirty.toString !== 'function') { throw new TypeError('toString is not a function'); } else { dirty = dirty.toString(); } } /* Check we can run. Otherwise fall back or ignore */ if (!DOMPurify.isSupported) { if (typeof window.toStaticHTML === 'object' || typeof window.toStaticHTML === 'function') { return window.toStaticHTML(dirty); } return dirty; } /* Assign config vars */ _parseConfig(cfg); /* Exit directly if we have nothing to do */ if (!RETURN_DOM && !WHOLE_DOCUMENT && dirty.indexOf('<') === -1) { return dirty; } /* Initialize the document to work on */ body = _initDocument(dirty); /* Check we have a DOM node from the data */ if (!body) { return RETURN_DOM ? null : ''; } /* Get node iterator */ nodeIterator = _createIterator(body); /* Now start iterating over the created document */ while ( (currentNode = nodeIterator.nextNode()) ) { /* Fix IE's strange behavior with manipulated textNodes #89 */ if (currentNode.nodeType === 3 && currentNode === oldNode) { continue; } /* Sanitize tags and elements */ if (_sanitizeElements(currentNode)) { continue; } /* Shadow DOM detected, sanitize it */ if (currentNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(currentNode.content); } /* Check attributes, sanitize if necessary */ _sanitizeAttributes(currentNode); oldNode = currentNode; } /* Return sanitized string or DOM */ if (RETURN_DOM) { if (RETURN_DOM_FRAGMENT) { returnNode = createDocumentFragment.call(body.ownerDocument); while (body.firstChild) { returnNode.appendChild(body.firstChild); } } else { returnNode = body; } if (RETURN_DOM_IMPORT) { /* adoptNode() is not used because internal state is not reset (e.g. the past names map of a HTMLFormElement), this is safe in theory but we would rather not risk another attack vector. The state that is cloned by importNode() is explicitly defined by the specs. */ returnNode = importNode.call(originalDocument, returnNode, true); } return returnNode; } return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; }; /** * addHook * Public method to add DOMPurify hooks * * @param {String} entryPoint * @param {Function} hookFunction */ DOMPurify.addHook = function(entryPoint, hookFunction) { if (typeof hookFunction !== 'function') { return; } hooks[entryPoint] = hooks[entryPoint] || []; hooks[entryPoint].push(hookFunction); }; /** * removeHook * Public method to remove a DOMPurify hook at a given entryPoint * (pops it from the stack of hooks if more are present) * * @param {String} entryPoint * @return void */ DOMPurify.removeHook = function(entryPoint) { if (hooks[entryPoint]) { hooks[entryPoint].pop(); } }; /** * removeHooks * Public method to remove all DOMPurify hooks at a given entryPoint * * @param {String} entryPoint * @return void */ DOMPurify.removeHooks = function(entryPoint) { if (hooks[entryPoint]) { hooks[entryPoint] = []; } }; /** * removeAllHooks * Public method to remove all DOMPurify hooks * * @return void */ DOMPurify.removeAllHooks = function() { hooks = []; }; return DOMPurify; })); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/plugins/sortable.js ================================================ /**! * KvSortable * @author RubaXa * @license MIT * * Changed kvsortable plugin naming to prevent conflict with JQuery UI Sortable * @author Kartik Visweswaran */ (function kvsortableModule(factory) { "use strict"; if (typeof define === "function" && define.amd) { define(factory); } else if (typeof module != "undefined" && typeof module.exports != "undefined") { module.exports = factory(); } else { /* jshint sub:true */ window["KvSortable"] = factory(); } })(function kvsortableFactory() { "use strict"; if (typeof window === "undefined" || !window.document) { return function kvsortableError() { throw new Error("KvSortable.js requires a window with a document"); }; } var dragEl, parentEl, ghostEl, cloneEl, rootEl, nextEl, lastDownEl, scrollEl, scrollParentEl, scrollCustomFn, lastEl, lastCSS, lastParentCSS, oldIndex, newIndex, activeGroup, putKvSortable, autoScroll = {}, tapEvt, touchEvt, moved, /** @const */ R_SPACE = /\s+/g, R_FLOAT = /left|right|inline/, expando = 'KvSortable' + (new Date).getTime(), win = window, document = win.document, parseInt = win.parseInt, setTimeout = win.setTimeout, $ = win.jQuery || win.Zepto, Polymer = win.Polymer, captureMode = false, passiveMode = false, supportDraggable = ('draggable' in document.createElement('div')), supportCssPointerEvents = (function (el) { // false when IE11 if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { return false; } el = document.createElement('x'); el.style.cssText = 'pointer-events:auto'; return el.style.pointerEvents === 'auto'; })(), _silent = false, abs = Math.abs, min = Math.min, savedInputChecked = [], touchDragOverListeners = [], _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 if (rootEl && options.scroll) { var _this = rootEl[expando], el, rect, sens = options.scrollSensitivity, speed = options.scrollSpeed, x = evt.clientX, y = evt.clientY, winWidth = window.innerWidth, winHeight = window.innerHeight, vx, vy, scrollOffsetX, scrollOffsetY ; // Delect scrollEl if (scrollParentEl !== rootEl) { scrollEl = options.scroll; scrollParentEl = rootEl; scrollCustomFn = options.scrollFn; if (scrollEl === true) { scrollEl = rootEl; do { if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || (scrollEl.offsetHeight < scrollEl.scrollHeight) ) { break; } /* jshint boss:true */ } while (scrollEl = scrollEl.parentNode); } } if (scrollEl) { el = scrollEl; rect = scrollEl.getBoundingClientRect(); vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); } if (!(vx || vy)) { vx = (winWidth - x <= sens) - (x <= sens); vy = (winHeight - y <= sens) - (y <= sens); /* jshint expr:true */ (vx || vy) && (el = win); } if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { autoScroll.el = el; autoScroll.vx = vx; autoScroll.vy = vy; clearInterval(autoScroll.pid); if (el) { autoScroll.pid = setInterval(function () { scrollOffsetY = vy ? vy * speed : 0; scrollOffsetX = vx ? vx * speed : 0; if ('function' === typeof(scrollCustomFn)) { return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt); } if (el === win) { win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); } else { el.scrollTop += scrollOffsetY; el.scrollLeft += scrollOffsetX; } }, 24); } } } }, 30), _prepareGroup = function (options) { function toFn(value, pull) { if (value === void 0 || value === true) { value = group.name; } if (typeof value === 'function') { return value; } else { return function (to, from) { var fromGroup = from.options.group.name; return pull ? value : value && (value.join ? value.indexOf(fromGroup) > -1 : (fromGroup == value) ); }; } } var group = {}; var originalGroup = options.group; if (!originalGroup || typeof originalGroup != 'object') { originalGroup = {name: originalGroup}; } group.name = originalGroup.name; group.checkPull = toFn(originalGroup.pull, true); group.checkPut = toFn(originalGroup.put); group.revertClone = originalGroup.revertClone; options.group = group; } ; // Detect support a passive mode try { window.addEventListener('test', null, Object.defineProperty({}, 'passive', { get: function () { // `false`, because everything starts to work incorrectly and instead of d'n'd, // begins the page has scrolled. passiveMode = false; captureMode = { capture: false, passive: passiveMode }; } })); } catch (err) {} /** * @class KvSortable * @param {HTMLElement} el * @param {Object} [options] */ function KvSortable(el, options) { if (!(el && el.nodeType && el.nodeType === 1)) { throw 'KvSortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); } this.el = el; // root element this.options = options = _extend({}, options); // Export instance el[expando] = this; // Default options var defaults = { group: Math.random(), sort: true, disabled: false, store: null, handle: null, scroll: true, scrollSensitivity: 30, scrollSpeed: 10, draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', ghostClass: 'kvsortable-ghost', chosenClass: 'kvsortable-chosen', dragClass: 'kvsortable-drag', ignore: 'a, img', filter: null, preventOnFilter: true, animation: 0, setData: function (dataTransfer, dragEl) { dataTransfer.setData('Text', dragEl.textContent); }, dropBubble: false, dragoverBubble: false, dataIdAttr: 'data-id', delay: 0, forceFallback: false, fallbackClass: 'kvsortable-fallback', fallbackOnBody: false, fallbackTolerance: 0, fallbackOffset: {x: 0, y: 0}, supportPointer: KvSortable.supportPointer !== false }; // Set default options for (var name in defaults) { !(name in options) && (options[name] = defaults[name]); } _prepareGroup(options); // Bind all private methods for (var fn in this) { if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { this[fn] = this[fn].bind(this); } } // Setup drag mode this.nativeDraggable = options.forceFallback ? false : supportDraggable; // Bind events _on(el, 'mousedown', this._onTapStart); _on(el, 'touchstart', this._onTapStart); options.supportPointer && _on(el, 'pointerdown', this._onTapStart); if (this.nativeDraggable) { _on(el, 'dragover', this); _on(el, 'dragenter', this); } touchDragOverListeners.push(this._onDragOver); // Restore sorting options.store && this.sort(options.store.get(this)); } KvSortable.prototype = /** @lends KvSortable.prototype */ { constructor: KvSortable, _onTapStart: function (/** Event|TouchEvent */evt) { var _this = this, el = this.el, options = this.options, preventOnFilter = options.preventOnFilter, type = evt.type, touch = evt.touches && evt.touches[0], target = (touch || evt).target, originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0]) || target, filter = options.filter, startIndex; _saveInputCheckedState(el); // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. if (dragEl) { return; } if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { return; // only left button or enabled } // cancel dnd if original target is content editable if (originalTarget.isContentEditable) { return; } target = _closest(target, options.draggable, el); if (!target) { return; } if (lastDownEl === target) { // Ignoring duplicate `down` return; } // Get the index of the dragged element within its parent startIndex = _index(target, options.draggable); // Check filter if (typeof filter === 'function') { if (filter.call(this, evt, target, this)) { _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); preventOnFilter && evt.preventDefault(); return; // cancel dnd } } else if (filter) { filter = filter.split(',').some(function (criteria) { criteria = _closest(originalTarget, criteria.trim(), el); if (criteria) { _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); return true; } }); if (filter) { preventOnFilter && evt.preventDefault(); return; // cancel dnd } } if (options.handle && !_closest(originalTarget, options.handle, el)) { return; } // Prepare `dragstart` this._prepareDragStart(evt, touch, target, startIndex); }, _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { var _this = this, el = _this.el, options = _this.options, ownerDocument = el.ownerDocument, dragStartFn; if (target && !dragEl && (target.parentNode === el)) { tapEvt = evt; rootEl = el; dragEl = target; parentEl = dragEl.parentNode; nextEl = dragEl.nextSibling; lastDownEl = target; activeGroup = options.group; oldIndex = startIndex; this._lastX = (touch || evt).clientX; this._lastY = (touch || evt).clientY; dragEl.style['will-change'] = 'all'; dragStartFn = function () { // Delayed drag has been triggered // we can re-enable the events: touchmove/mousemove _this._disableDelayedDrag(); // Make the element draggable dragEl.draggable = _this.nativeDraggable; // Chosen item _toggleClass(dragEl, options.chosenClass, true); // Bind the events: dragstart/dragend _this._triggerDragStart(evt, touch); // Drag start event _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); }; // Disable "draggable" options.ignore.split(',').forEach(function (criteria) { _find(dragEl, criteria.trim(), _disableDraggable); }); _on(ownerDocument, 'mouseup', _this._onDrop); _on(ownerDocument, 'touchend', _this._onDrop); _on(ownerDocument, 'touchcancel', _this._onDrop); _on(ownerDocument, 'selectstart', _this); options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); if (options.delay) { // If the user moves the pointer or let go the click or touch // before the delay has been reached: // disable the delayed drag _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); _on(ownerDocument, 'touchend', _this._disableDelayedDrag); _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); _on(ownerDocument, 'mousemove', _this._disableDelayedDrag); _on(ownerDocument, 'touchmove', _this._disableDelayedDrag); options.supportPointer && _on(ownerDocument, 'pointermove', _this._disableDelayedDrag); _this._dragStartTimer = setTimeout(dragStartFn, options.delay); } else { dragStartFn(); } } }, _disableDelayedDrag: function () { var ownerDocument = this.el.ownerDocument; clearTimeout(this._dragStartTimer); _off(ownerDocument, 'mouseup', this._disableDelayedDrag); _off(ownerDocument, 'touchend', this._disableDelayedDrag); _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); _off(ownerDocument, 'mousemove', this._disableDelayedDrag); _off(ownerDocument, 'touchmove', this._disableDelayedDrag); _off(ownerDocument, 'pointermove', this._disableDelayedDrag); }, _triggerDragStart: function (/** Event */evt, /** Touch */touch) { touch = touch || (evt.pointerType == 'touch' ? evt : null); if (touch) { // Touch device support tapEvt = { target: dragEl, clientX: touch.clientX, clientY: touch.clientY }; this._onDragStart(tapEvt, 'touch'); } else if (!this.nativeDraggable) { this._onDragStart(tapEvt, true); } else { _on(dragEl, 'dragend', this); _on(rootEl, 'dragstart', this._onDragStart); } try { if (document.selection) { // Timeout neccessary for IE9 _nextTick(function () { document.selection.empty(); }); } else { window.getSelection().removeAllRanges(); } } catch (err) { } }, _dragStarted: function () { if (rootEl && dragEl) { var options = this.options; // Apply effect _toggleClass(dragEl, options.ghostClass, true); _toggleClass(dragEl, options.dragClass, false); KvSortable.active = this; // Drag start event _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); } else { this._nulling(); } }, _emulateDragOver: function () { if (touchEvt) { if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { return; } this._lastX = touchEvt.clientX; this._lastY = touchEvt.clientY; if (!supportCssPointerEvents) { _css(ghostEl, 'display', 'none'); } var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); var parent = target; var i = touchDragOverListeners.length; if (target && target.shadowRoot) { target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); parent = target; } if (parent) { do { if (parent[expando]) { while (i--) { touchDragOverListeners[i]({ clientX: touchEvt.clientX, clientY: touchEvt.clientY, target: target, rootEl: parent }); } break; } target = parent; // store last element } /* jshint boss:true */ while (parent = parent.parentNode); } if (!supportCssPointerEvents) { _css(ghostEl, 'display', ''); } } }, _onTouchMove: function (/**TouchEvent*/evt) { if (tapEvt) { var options = this.options, fallbackTolerance = options.fallbackTolerance, fallbackOffset = options.fallbackOffset, touch = evt.touches ? evt.touches[0] : evt, dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; // only set the status to dragging, when we are actually dragging if (!KvSortable.active) { if (fallbackTolerance && min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance ) { return; } this._dragStarted(); } // as well as creating the ghost element on the document body this._appendGhost(); moved = true; touchEvt = touch; _css(ghostEl, 'webkitTransform', translate3d); _css(ghostEl, 'mozTransform', translate3d); _css(ghostEl, 'msTransform', translate3d); _css(ghostEl, 'transform', translate3d); evt.preventDefault(); } }, _appendGhost: function () { if (!ghostEl) { var rect = dragEl.getBoundingClientRect(), css = _css(dragEl), options = this.options, ghostRect; ghostEl = dragEl.cloneNode(true); _toggleClass(ghostEl, options.ghostClass, false); _toggleClass(ghostEl, options.fallbackClass, true); _toggleClass(ghostEl, options.dragClass, true); _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); _css(ghostEl, 'width', rect.width); _css(ghostEl, 'height', rect.height); _css(ghostEl, 'opacity', '0.8'); _css(ghostEl, 'position', 'fixed'); _css(ghostEl, 'zIndex', '100000'); _css(ghostEl, 'pointerEvents', 'none'); options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); // Fixing dimensions. ghostRect = ghostEl.getBoundingClientRect(); _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); } }, _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { var _this = this; var dataTransfer = evt.dataTransfer; var options = _this.options; _this._offUpEvents(); if (activeGroup.checkPull(_this, _this, dragEl, evt)) { cloneEl = _clone(dragEl); cloneEl.draggable = false; cloneEl.style['will-change'] = ''; _css(cloneEl, 'display', 'none'); _toggleClass(cloneEl, _this.options.chosenClass, false); // #1143: IFrame support workaround _this._cloneId = _nextTick(function () { rootEl.insertBefore(cloneEl, dragEl); _dispatchEvent(_this, rootEl, 'clone', dragEl); }); } _toggleClass(dragEl, options.dragClass, true); if (useFallback) { if (useFallback === 'touch') { // Bind touch events _on(document, 'touchmove', _this._onTouchMove); _on(document, 'touchend', _this._onDrop); _on(document, 'touchcancel', _this._onDrop); if (options.supportPointer) { _on(document, 'pointermove', _this._onTouchMove); _on(document, 'pointerup', _this._onDrop); } } else { // Old brwoser _on(document, 'mousemove', _this._onTouchMove); _on(document, 'mouseup', _this._onDrop); } _this._loopId = setInterval(_this._emulateDragOver, 50); } else { if (dataTransfer) { dataTransfer.effectAllowed = 'move'; options.setData && options.setData.call(_this, dataTransfer, dragEl); } _on(document, 'drop', _this); // #1143: Бывает элемент с IFrame внутри блокирует `drop`, // поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd. // Breaking Chrome 62+ // _on(document, 'mouseover', _this); _this._dragStartId = _nextTick(_this._dragStarted); } }, _onDragOver: function (/**Event*/evt) { var el = this.el, target, dragRect, targetRect, revert, options = this.options, group = options.group, activeKvSortable = KvSortable.active, isOwner = (activeGroup === group), isMovingBetweenKvSortable = false, canSort = options.sort; if (evt.preventDefault !== void 0) { evt.preventDefault(); !options.dragoverBubble && evt.stopPropagation(); } if (dragEl.animated) { return; } moved = true; if (activeKvSortable && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list : ( putKvSortable === this || ( (activeKvSortable.lastPullMode = activeGroup.checkPull(this, activeKvSortable, dragEl, evt)) && group.checkPut(this, activeKvSortable, dragEl, evt) ) ) ) && (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback ) { // Smart auto-scrolling _autoScroll(evt, options, this.el); if (_silent) { return; } target = _closest(evt.target, options.draggable, el); dragRect = dragEl.getBoundingClientRect(); if (putKvSortable !== this) { putKvSortable = this; isMovingBetweenKvSortable = true; } if (revert) { _cloneHide(activeKvSortable, true); parentEl = rootEl; // actualization if (cloneEl || nextEl) { rootEl.insertBefore(dragEl, cloneEl || nextEl); } else if (!canSort) { rootEl.appendChild(dragEl); } return; } if ((el.children.length === 0) || (el.children[0] === ghostEl) || (el === evt.target) && (_ghostIsLast(el, evt)) ) { //assign target only if condition is true if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { target = el.lastElementChild; } if (target) { if (target.animated) { return; } targetRect = target.getBoundingClientRect(); } _cloneHide(activeKvSortable, isOwner); if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { if (!dragEl.contains(el)) { el.appendChild(dragEl); parentEl = el; // actualization } this._animate(dragRect, dragEl); target && this._animate(targetRect, target); } } else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { if (lastEl !== target) { lastEl = target; lastCSS = _css(target); lastParentCSS = _css(target.parentNode); } targetRect = target.getBoundingClientRect(); var width = targetRect.right - targetRect.left, height = targetRect.bottom - targetRect.top, floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display) || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), isWide = (target.offsetWidth > dragEl.offsetWidth), isLong = (target.offsetHeight > dragEl.offsetHeight), halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, nextSibling = target.nextElementSibling, after = false ; if (floating) { var elTop = dragEl.offsetTop, tgTop = target.offsetTop; if (elTop === tgTop) { after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; } else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) { after = (evt.clientY - targetRect.top) / height > 0.5; } else { after = tgTop > elTop; } } else if (!isMovingBetweenKvSortable) { after = (nextSibling !== dragEl) && !isLong || halfway && isLong; } var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); if (moveVector !== false) { if (moveVector === 1 || moveVector === -1) { after = (moveVector === 1); } _silent = true; setTimeout(_unsilent, 30); _cloneHide(activeKvSortable, isOwner); if (!dragEl.contains(el)) { if (after && !nextSibling) { el.appendChild(dragEl); } else { target.parentNode.insertBefore(dragEl, after ? nextSibling : target); } } parentEl = dragEl.parentNode; // actualization this._animate(dragRect, dragEl); this._animate(targetRect, target); } } } }, _animate: function (prevRect, target) { var ms = this.options.animation; if (ms) { var currentRect = target.getBoundingClientRect(); if (prevRect.nodeType === 1) { prevRect = prevRect.getBoundingClientRect(); } _css(target, 'transition', 'none'); _css(target, 'transform', 'translate3d(' + (prevRect.left - currentRect.left) + 'px,' + (prevRect.top - currentRect.top) + 'px,0)' ); target.offsetWidth; // repaint _css(target, 'transition', 'all ' + ms + 'ms'); _css(target, 'transform', 'translate3d(0,0,0)'); clearTimeout(target.animated); target.animated = setTimeout(function () { _css(target, 'transition', ''); _css(target, 'transform', ''); target.animated = false; }, ms); } }, _offUpEvents: function () { var ownerDocument = this.el.ownerDocument; _off(document, 'touchmove', this._onTouchMove); _off(document, 'pointermove', this._onTouchMove); _off(ownerDocument, 'mouseup', this._onDrop); _off(ownerDocument, 'touchend', this._onDrop); _off(ownerDocument, 'pointerup', this._onDrop); _off(ownerDocument, 'touchcancel', this._onDrop); _off(ownerDocument, 'pointercancel', this._onDrop); _off(ownerDocument, 'selectstart', this); }, _onDrop: function (/**Event*/evt) { var el = this.el, options = this.options; clearInterval(this._loopId); clearInterval(autoScroll.pid); clearTimeout(this._dragStartTimer); _cancelNextTick(this._cloneId); _cancelNextTick(this._dragStartId); // Unbind events _off(document, 'mouseover', this); _off(document, 'mousemove', this._onTouchMove); if (this.nativeDraggable) { _off(document, 'drop', this); _off(el, 'dragstart', this._onDragStart); } this._offUpEvents(); if (evt) { if (moved) { evt.preventDefault(); !options.dropBubble && evt.stopPropagation(); } ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); if (rootEl === parentEl || KvSortable.active.lastPullMode !== 'clone') { // Remove clone cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); } if (dragEl) { if (this.nativeDraggable) { _off(dragEl, 'dragend', this); } _disableDraggable(dragEl); dragEl.style['will-change'] = ''; // Remove class's _toggleClass(dragEl, this.options.ghostClass, false); _toggleClass(dragEl, this.options.chosenClass, false); // Drag stop event _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex); if (rootEl !== parentEl) { newIndex = _index(dragEl, options.draggable); if (newIndex >= 0) { // Add event _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex); // Remove event _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex); // drag from one list and drop into another _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); } } else { if (dragEl.nextSibling !== nextEl) { // Get the index of the dragged element within its parent newIndex = _index(dragEl, options.draggable); if (newIndex >= 0) { // drag & drop within the same list _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex); _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); } } } if (KvSortable.active) { /* jshint eqnull:true */ if (newIndex == null || newIndex === -1) { newIndex = oldIndex; } _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex); // Save sorting this.save(); } } } this._nulling(); }, _nulling: function() { rootEl = dragEl = parentEl = ghostEl = nextEl = cloneEl = lastDownEl = scrollEl = scrollParentEl = tapEvt = touchEvt = moved = newIndex = lastEl = lastCSS = putKvSortable = activeGroup = KvSortable.active = null; savedInputChecked.forEach(function (el) { el.checked = true; }); savedInputChecked.length = 0; }, handleEvent: function (/**Event*/evt) { switch (evt.type) { case 'drop': case 'dragend': this._onDrop(evt); break; case 'dragover': case 'dragenter': if (dragEl) { this._onDragOver(evt); _globalDragOver(evt); } break; case 'mouseover': this._onDrop(evt); break; case 'selectstart': evt.preventDefault(); break; } }, /** * Serializes the item into an array of string. * @returns {String[]} */ toArray: function () { var order = [], el, children = this.el.children, i = 0, n = children.length, options = this.options; for (; i < n; i++) { el = children[i]; if (_closest(el, options.draggable, this.el)) { order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); } } return order; }, /** * Sorts the elements according to the array. * @param {String[]} order order of the items */ sort: function (order) { var items = {}, rootEl = this.el; this.toArray().forEach(function (id, i) { var el = rootEl.children[i]; if (_closest(el, this.options.draggable, rootEl)) { items[id] = el; } }, this); order.forEach(function (id) { if (items[id]) { rootEl.removeChild(items[id]); rootEl.appendChild(items[id]); } }); }, /** * Save the current sorting */ save: function () { var store = this.options.store; store && store.set(this); }, /** * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. * @param {HTMLElement} el * @param {String} [selector] default: `options.draggable` * @returns {HTMLElement|null} */ closest: function (el, selector) { return _closest(el, selector || this.options.draggable, this.el); }, /** * Set/get option * @param {string} name * @param {*} [value] * @returns {*} */ option: function (name, value) { var options = this.options; if (value === void 0) { return options[name]; } else { options[name] = value; if (name === 'group') { _prepareGroup(options); } } }, /** * Destroy */ destroy: function () { var el = this.el; el[expando] = null; _off(el, 'mousedown', this._onTapStart); _off(el, 'touchstart', this._onTapStart); _off(el, 'pointerdown', this._onTapStart); if (this.nativeDraggable) { _off(el, 'dragover', this); _off(el, 'dragenter', this); } // Remove draggable attributes Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { el.removeAttribute('draggable'); }); touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); this._onDrop(); this.el = el = null; } }; function _cloneHide(kvsortable, state) { if (kvsortable.lastPullMode !== 'clone') { state = true; } if (cloneEl && (cloneEl.state !== state)) { _css(cloneEl, 'display', state ? 'none' : ''); if (!state) { if (cloneEl.state) { if (kvsortable.options.group.revertClone) { rootEl.insertBefore(cloneEl, nextEl); kvsortable._animate(dragEl, cloneEl); } else { rootEl.insertBefore(cloneEl, dragEl); } } } cloneEl.state = state; } } function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { if (el) { ctx = ctx || document; do { if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { return el; } /* jshint boss:true */ } while (el = _getParentOrHost(el)); } return null; } function _getParentOrHost(el) { var parent = el.host; return (parent && parent.nodeType) ? parent : el.parentNode; } function _globalDragOver(/**Event*/evt) { if (evt.dataTransfer) { evt.dataTransfer.dropEffect = 'move'; } evt.preventDefault(); } function _on(el, event, fn) { el.addEventListener(event, fn, captureMode); } function _off(el, event, fn) { el.removeEventListener(event, fn, captureMode); } function _toggleClass(el, name, state) { if (el) { if (el.classList) { el.classList[state ? 'add' : 'remove'](name); } else { var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); } } } function _css(el, prop, val) { var style = el && el.style; if (style) { if (val === void 0) { if (document.defaultView && document.defaultView.getComputedStyle) { val = document.defaultView.getComputedStyle(el, ''); } else if (el.currentStyle) { val = el.currentStyle; } return prop === void 0 ? val : val[prop]; } else { if (!(prop in style)) { prop = '-webkit-' + prop; } style[prop] = val + (typeof val === 'string' ? '' : 'px'); } } } function _find(ctx, tagName, iterator) { if (ctx) { var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; if (iterator) { for (; i < n; i++) { iterator(list[i], i); } } return list; } return []; } function _dispatchEvent(kvsortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex) { kvsortable = (kvsortable || rootEl[expando]); var evt = document.createEvent('Event'), options = kvsortable.options, onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); evt.initEvent(name, true, true); evt.to = toEl || rootEl; evt.from = fromEl || rootEl; evt.item = targetEl || rootEl; evt.clone = cloneEl; evt.oldIndex = startIndex; evt.newIndex = newIndex; rootEl.dispatchEvent(evt); if (options[onName]) { options[onName].call(kvsortable, evt); } } function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { var evt, kvsortable = fromEl[expando], onMoveFn = kvsortable.options.onMove, retVal; evt = document.createEvent('Event'); evt.initEvent('move', true, true); evt.to = toEl; evt.from = fromEl; evt.dragged = dragEl; evt.draggedRect = dragRect; evt.related = targetEl || toEl; evt.relatedRect = targetRect || toEl.getBoundingClientRect(); evt.willInsertAfter = willInsertAfter; fromEl.dispatchEvent(evt); if (onMoveFn) { retVal = onMoveFn.call(kvsortable, evt, originalEvt); } return retVal; } function _disableDraggable(el) { el.draggable = false; } function _unsilent() { _silent = false; } /** @returns {HTMLElement|false} */ function _ghostIsLast(el, evt) { var lastEl = el.lastElementChild, rect = lastEl.getBoundingClientRect(); // 5 — min delta // abs — нельзя добавлять, а то глюки при наведении сверху return (evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.left + rect.width) > 5); } /** * Generate id * @param {HTMLElement} el * @returns {String} * @private */ function _generateId(el) { var str = el.tagName + el.className + el.src + el.href + el.textContent, i = str.length, sum = 0; while (i--) { sum += str.charCodeAt(i); } return sum.toString(36); } /** * Returns the index of an element within its parent for a selected set of * elements * @param {HTMLElement} el * @param {selector} selector * @return {number} */ function _index(el, selector) { var index = 0; if (!el || !el.parentNode) { return -1; } while (el && (el = el.previousElementSibling)) { if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { index++; } } return index; } function _matches(/**HTMLElement*/el, /**String*/selector) { if (el) { selector = selector.split('.'); var tag = selector.shift().toUpperCase(), re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g'); return ( (tag === '' || el.nodeName.toUpperCase() == tag) && (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length) ); } return false; } function _throttle(callback, ms) { var args, _this; return function () { if (args === void 0) { args = arguments; _this = this; setTimeout(function () { if (args.length === 1) { callback.call(_this, args[0]); } else { callback.apply(_this, args); } args = void 0; }, ms); } }; } function _extend(dst, src) { if (dst && src) { for (var key in src) { if (src.hasOwnProperty(key)) { dst[key] = src[key]; } } } return dst; } function _clone(el) { if (Polymer && Polymer.dom) { return Polymer.dom(el).cloneNode(true); } else if ($) { return $(el).clone(true)[0]; } else { return el.cloneNode(true); } } function _saveInputCheckedState(root) { var inputs = root.getElementsByTagName('input'); var idx = inputs.length; while (idx--) { var el = inputs[idx]; el.checked && savedInputChecked.push(el); } } function _nextTick(fn) { return setTimeout(fn, 0); } function _cancelNextTick(id) { return clearTimeout(id); } // Fixed #973: _on(document, 'touchmove', function (evt) { if (KvSortable.active) { evt.preventDefault(); } }); // Export utils KvSortable.utils = { on: _on, off: _off, css: _css, find: _find, is: function (el, selector) { return !!_closest(el, selector, el); }, extend: _extend, throttle: _throttle, closest: _closest, toggleClass: _toggleClass, clone: _clone, index: _index, nextTick: _nextTick, cancelNextTick: _cancelNextTick }; /** * Create kvsortable instance * @param {HTMLElement} el * @param {Object} [options] */ KvSortable.create = function (el, options) { return new KvSortable(el, options); }; // Export KvSortable.version = '1.7.0'; return KvSortable; }); /** * jQuery plugin for KvSortable */ (function (factory) { "use strict"; if (typeof define === "function" && define.amd) { define(["jquery"], factory); } else { /* jshint sub:true */ factory(jQuery); } })(function ($) { "use strict"; $.fn.kvsortable = function (options) { var retVal, args = arguments; this.each(function () { var $el = $(this), kvsortable = $el.data('kvsortable'); if (!kvsortable && (options instanceof Object || !options)) { kvsortable = new KvSortable(this, options); $el.data('kvsortable', kvsortable); } if (kvsortable) { if (options === 'widget') { retVal = kvsortable; } else if (options === 'destroy') { kvsortable.destroy(); $el.removeData('kvsortable'); } else if (typeof kvsortable[options] === 'function') { retVal = kvsortable[options].apply(kvsortable, [].slice.call(args, 1)); } else if (options in kvsortable.options) { retVal = kvsortable.option.apply(kvsortable, args); } } }); return (retVal === void 0) ? this : retVal; }; }); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/explorer/theme.css ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee Explorer theme style for bootstrap-fileinput. Load this theme file after loading `fileinput.css`. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ .theme-explorer .file-upload-indicator, .theme-explorer .file-drag-handle, .theme-explorer .explorer-frame .kv-file-content, .theme-explorer .file-actions, .explorer-frame .file-preview-other { text-align: center; } .theme-explorer .file-thumb-progress .progress, .theme-explorer .file-thumb-progress .progress-bar { height: 13px; font-size: 11px; line-height: 13px; } .theme-explorer .file-upload-indicator, .theme-explorer .file-drag-handle { position: absolute; display: inline-block; top: 0; right: 3px; width: 16px; height: 16px; font-size: 16px; } .theme-explorer .file-thumb-progress .progress, .theme-explorer .explorer-caption { display: block; } .theme-explorer .explorer-frame td { vertical-align: middle; text-align: left; } .theme-explorer .explorer-frame .kv-file-content { width: 80px; height: 80px; padding: 5px; } .theme-explorer .file-actions-cell { position: relative; width: 120px; padding: 0; } .theme-explorer .file-thumb-progress .progress { margin-top: 5px; } .theme-explorer .explorer-caption { color: #777; } .theme-explorer .kvsortable-ghost { opacity: 0.6; background: #e1edf7; border: 2px solid #a1abff; } .theme-explorer .file-preview .table { margin: 0; } .theme-explorer .file-error-message ul { padding: 5px 0 0 20px; } .explorer-frame .file-preview-text { display: inline-block; color: #428bca; border: 1px solid #ddd; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; outline: none; padding: 8px; resize: none; } .explorer-frame .file-preview-html { display: inline-block; border: 1px solid #ddd; padding: 8px; overflow: auto; } .explorer-frame .file-other-icon { font-size: 2.6em; } @media only screen and (max-width: 767px) { .theme-explorer .table, .theme-explorer .table tbody, .theme-explorer .table tr, .theme-explorer .table td { display: block; width: 100% !important; } .theme-explorer .table { border: none; } .theme-explorer .table tr { margin-top: 5px; } .theme-explorer .table tr:first-child { margin-top: 0; } .theme-explorer .table td { text-align: center; } .theme-explorer .table .kv-file-content { border-bottom: none; padding: 4px; margin: 0; } .theme-explorer .table .kv-file-content .file-preview-image { max-width: 100%; font-size: 20px; } .theme-explorer .file-details-cell { border-top: none; border-bottom: none; padding-top: 0; margin: 0; } .theme-explorer .file-actions-cell { border-top: none; padding-bottom: 4px; } .theme-explorer .explorer-frame .explorer-caption { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; left: 0; right: 0; margin: auto; } } /*noinspection CssOverwrittenProperties*/ .file-zoom-dialog .explorer-frame .file-other-icon { font-size: 22em; font-size: 50vmin; } ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/explorer/theme.js ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee Explorer theme configuration for bootstrap-fileinput. Load this theme file after loading `fileinput.js`. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ (function ($) { "use strict"; var teTagBef = '\n' + ' {close}' + '
    \n' + ' \n' + '
    \n' + '
    ' + '
    \n' + '
    \n' + '
    \n' + '
    ', footer: '
    {caption}
    ' + '{size}{progress}{indicator} {actions}', actions: '{drag}\n' + '
    \n' + ' \n' + '
    ', zoomCache: '' + '{zoomContent}
    ' }, previewMarkupTags: { tagBefore1: teTagBef + '>' + teContent, tagBefore2: teTagBef + ' title="{caption}">' + teContent, tagAfter: '\n{footer}\n' }, previewSettings: { image: {height: "60px"}, html: {width: "100px", height: "60px"}, text: {width: "100px", height: "60px"}, video: {width: "auto", height: "60px"}, audio: {width: "auto", height: "60px"}, flash: {width: "100%", height: "60px"}, object: {width: "100%", height: "60px"}, pdf: {width: "100px", height: "60px"}, other: {width: "100%", height: "60px"} }, frameClass: 'explorer-frame' }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/explorer-fa/theme.css ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee Explorer Font Awesome theme style for bootstrap-fileinput. Load this theme file after loading `fileinput.css`. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ .theme-explorer-fa .file-upload-indicator, .theme-explorer-fa .file-drag-handle, .theme-explorer-fa .explorer-frame .kv-file-content, .theme-explorer-fa .file-actions, .explorer-frame .file-preview-other { text-align: center; } .theme-explorer-fa .file-thumb-progress .progress, .theme-explorer-fa .file-thumb-progress .progress-bar { height: 13px; font-size: 11px; line-height: 13px; } .theme-explorer-fa .file-upload-indicator, .theme-explorer-fa .file-drag-handle { position: absolute; display: inline-block; top: 0; right: 3px; width: 16px; height: 16px; font-size: 16px; } .theme-explorer-fa .file-thumb-progress .progress, .theme-explorer-fa .explorer-caption { display: block; } .theme-explorer-fa .explorer-frame td { vertical-align: middle; text-align: left; } .theme-explorer-fa .explorer-frame .kv-file-content { width: 80px; height: 80px; padding: 5px; } .theme-explorer-fa .file-actions-cell { position: relative; width: 120px; padding: 0; } .theme-explorer-fa .file-thumb-progress .progress { margin-top: 5px; } .theme-explorer-fa .explorer-caption { color: #777; } .theme-explorer-fa .kvsortable-ghost { opacity: 0.6; background: #e1edf7; border: 2px solid #a1abff; } .theme-explorer-fa .file-preview .table { margin: 0; } .theme-explorer-fa .file-error-message ul { padding: 5px 0 0 20px; } .explorer-frame .file-preview-text { display: inline-block; color: #428bca; border: 1px solid #ddd; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; outline: none; padding: 8px; resize: none; } .explorer-frame .file-preview-html { display: inline-block; border: 1px solid #ddd; padding: 8px; overflow: auto; } .explorer-frame .file-other-icon { font-size: 2.6em; } @media only screen and (max-width: 767px) { .theme-explorer-fa .table, .theme-explorer-fa .table tbody, .theme-explorer-fa .table tr, .theme-explorer-fa .table td { display: block; width: 100% !important; } .theme-explorer-fa .table { border: none; } .theme-explorer-fa .table tr { margin-top: 5px; } .theme-explorer-fa .table tr:first-child { margin-top: 0; } .theme-explorer-fa .table td { text-align: center; } .theme-explorer-fa .table .kv-file-content { border-bottom: none; padding: 4px; margin: 0; } .theme-explorer-fa .table .kv-file-content .file-preview-image { max-width: 100%; font-size: 20px; } .theme-explorer-fa .file-details-cell { border-top: none; border-bottom: none; padding-top: 0; margin: 0; } .theme-explorer-fa .file-actions-cell { border-top: none; padding-bottom: 4px; } .theme-explorer-fa .explorer-frame .explorer-caption { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; left: 0; right: 0; margin: auto; } } /*noinspection CssOverwrittenProperties*/ .file-zoom-dialog .explorer-frame .file-other-icon { font-size: 22em; font-size: 50vmin; } ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/explorer-fa/theme.js ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Krajee Explorer Font Awesome theme configuration for bootstrap-fileinput. * Load this theme file after loading `fileinput.js`. Ensure that * font awesome assets and CSS are loaded on the page as well. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ (function ($) { "use strict"; var teTagBef = '\n' + ' {close}' + '
    \n' + ' \n' + '
    \n' + '
    ' + '
    \n' + '
    \n' + '
    \n' + '
    ', footer: '
    {caption}
    ' + '{size}{progress}{indicator} {actions}', actions: '{drag}\n' + '
    \n' + ' \n' + '
    ', zoomCache: '' + '{zoomContent}
    ', fileIcon: ' ' }, previewMarkupTags: { tagBefore1: teTagBef + '>' + teContent, tagBefore2: teTagBef + ' title="{caption}">' + teContent, tagAfter: '\n{footer}\n' }, previewSettings: { image: {height: "60px"}, html: {width: "100px", height: "60px"}, text: {width: "100px", height: "60px"}, video: {width: "auto", height: "60px"}, audio: {width: "auto", height: "60px"}, flash: {width: "100%", height: "60px"}, object: {width: "100%", height: "60px"}, pdf: {width: "100px", height: "60px"}, other: {width: "100%", height: "60px"} }, frameClass: 'explorer-frame', fileActionSettings: { removeIcon: '', uploadIcon: '', uploadRetryIcon: '', zoomIcon: '', dragIcon: '', indicatorNew: '', indicatorSuccess: '', indicatorError: '', indicatorLoading: '' }, previewZoomButtonIcons: { prev: '', next: '', toggleheader: '', fullscreen: '', borderless: '', close: '' }, previewFileIcon: '', browseIcon: '', removeIcon: '', cancelIcon: '', uploadIcon: '', msgValidationErrorIcon: ' ' }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/fa/theme.js ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Font Awesome icon theme configuration for bootstrap-fileinput. Requires font awesome assets to be loaded. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ (function ($) { "use strict"; $.fn.fileinputThemes.fa = { fileActionSettings: { removeIcon: '', uploadIcon: '', uploadRetryIcon: '', zoomIcon: '', dragIcon: '', indicatorNew: '', indicatorSuccess: '', indicatorError: '', indicatorLoading: '' }, layoutTemplates: { fileIcon: ' ' }, previewZoomButtonIcons: { prev: '', next: '', toggleheader: '', fullscreen: '', borderless: '', close: '' }, previewFileIcon: '', browseIcon: '', removeIcon: '', cancelIcon: '', uploadIcon: '', msgValidationErrorIcon: ' ' }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/gly/theme.js ================================================ /*! * bootstrap-fileinput v4.4.7 * http://plugins.krajee.com/file-input * * Glyphicon (default) theme configuration for bootstrap-fileinput. * * Author: Kartik Visweswaran * Copyright: 2014 - 2018, Kartik Visweswaran, Krajee.com * * Licensed under the BSD 3-Clause * https://github.com/kartik-v/bootstrap-fileinput/blob/master/LICENSE.md */ (function ($) { "use strict"; $.fn.fileinputThemes.gly = { fileActionSettings: { removeIcon: '', uploadIcon: '', zoomIcon: '', dragIcon: '', indicatorNew: '', indicatorSuccess: '', indicatorError: '', indicatorLoading: '' }, layoutTemplates: { fileIcon: '' }, previewZoomButtonIcons: { prev: '', next: '', toggleheader: '', fullscreen: '', borderless: '', close: '' }, previewFileIcon: '', browseIcon: ' ', removeIcon: '', cancelIcon: '', uploadIcon: '', msgValidationErrorIcon: ' ' }; })(window.jQuery); ================================================ FILE: static/bootstrap/plugins/bootstrap-switch/css/bootstrap2/bootstrap-switch.css ================================================ /** * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. * * @version v3.3.4 * @homepage https://bttstrp.github.io/bootstrap-switch * @author Mattia Larentis (http://larentis.eu) * @license Apache-2.0 */ .clearfix { *zoom: 1; } .clearfix:before, .clearfix:after { display: table; content: ""; line-height: 0; } .clearfix:after { clear: both; } .hide-text { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .input-block-level { display: block; width: 100%; min-height: 30px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } .bootstrap-switch { display: inline-block; direction: ltr; cursor: pointer; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; border: 1px solid; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); position: relative; text-align: left; overflow: hidden; line-height: 8px; z-index: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; vertical-align: middle; -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -moz-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } .bootstrap-switch .bootstrap-switch-container { display: inline-block; top: 0; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -o-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .bootstrap-switch .bootstrap-switch-handle-on, .bootstrap-switch .bootstrap-switch-handle-off, .bootstrap-switch .bootstrap-switch-label { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; cursor: pointer; display: inline-block !important; padding-top: 4px; padding-bottom: 4px; padding-left: 8px; padding-right: 8px; font-size: 14px; line-height: 20px; } .bootstrap-switch .bootstrap-switch-handle-on, .bootstrap-switch .bootstrap-switch-handle-off { text-align: center; z-index: 1; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #005fcc; background-image: -moz-linear-gradient(top, #0044cc, #08c); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0044cc), to(#08c)); background-image: -webkit-linear-gradient(top, #0044cc, #08c); background-image: -o-linear-gradient(top, #0044cc, #08c); background-image: linear-gradient(to bottom, #0044cc, #08c); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0044cc', endColorstr='#ff0088cc', GradientType=0); border-color: #08c #08c #005580; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #08c; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary[disabled] { color: #fff; background-color: #08c; *background-color: #0077b3; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary.active { background-color: #006699 \9; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #41a7c5; background-image: -moz-linear-gradient(top, #2f96b4, #5bc0de); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#2f96b4), to(#5bc0de)); background-image: -webkit-linear-gradient(top, #2f96b4, #5bc0de); background-image: -o-linear-gradient(top, #2f96b4, #5bc0de); background-image: linear-gradient(to bottom, #2f96b4, #5bc0de); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2f96b4', endColorstr='#ff5bc0de', GradientType=0); border-color: #5bc0de #5bc0de #28a1c5; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #5bc0de; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info[disabled] { color: #fff; background-color: #5bc0de; *background-color: #46b8da; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info.active { background-color: #31b0d5 \9; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #58b058; background-image: -moz-linear-gradient(top, #51a351, #62c462); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#51a351), to(#62c462)); background-image: -webkit-linear-gradient(top, #51a351, #62c462); background-image: -o-linear-gradient(top, #51a351, #62c462); background-image: linear-gradient(to bottom, #51a351, #62c462); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff51a351', endColorstr='#ff62c462', GradientType=0); border-color: #62c462 #62c462 #3b9e3b; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #62c462; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success[disabled] { color: #fff; background-color: #62c462; *background-color: #4fbd4f; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success.active { background-color: #42b142 \9; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #f9a123; background-image: -moz-linear-gradient(top, #f89406, #fbb450); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f89406), to(#fbb450)); background-image: -webkit-linear-gradient(top, #f89406, #fbb450); background-image: -o-linear-gradient(top, #f89406, #fbb450); background-image: linear-gradient(to bottom, #f89406, #fbb450); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff89406', endColorstr='#fffbb450', GradientType=0); border-color: #fbb450 #fbb450 #f89406; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #fbb450; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning[disabled] { color: #fff; background-color: #fbb450; *background-color: #faa937; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning.active { background-color: #fa9f1e \9; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger { color: #fff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #d14641; background-image: -moz-linear-gradient(top, #bd362f, #ee5f5b); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#bd362f), to(#ee5f5b)); background-image: -webkit-linear-gradient(top, #bd362f, #ee5f5b); background-image: -o-linear-gradient(top, #bd362f, #ee5f5b); background-image: linear-gradient(to bottom, #bd362f, #ee5f5b); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffbd362f', endColorstr='#ffee5f5b', GradientType=0); border-color: #ee5f5b #ee5f5b #e51d18; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #ee5f5b; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger[disabled] { color: #fff; background-color: #ee5f5b; *background-color: #ec4844; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger.active { background-color: #e9322d \9; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default { color: #333; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); background-color: #f0f0f0; background-image: -moz-linear-gradient(top, #e6e6e6, #fff); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#e6e6e6), to(#fff)); background-image: -webkit-linear-gradient(top, #e6e6e6, #fff); background-image: -o-linear-gradient(top, #e6e6e6, #fff); background-image: linear-gradient(to bottom, #e6e6e6, #fff); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe6e6e6', endColorstr='#ffffffff', GradientType=0); border-color: #fff #fff #d9d9d9; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #fff; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default:hover, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default:focus, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default:focus, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default.active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default.disabled, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default.disabled, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default[disabled], .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default[disabled] { color: #333; background-color: #fff; *background-color: #f2f2f2; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default:active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default:active, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default.active, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default.active { background-color: #e6e6e6 \9; } .bootstrap-switch .bootstrap-switch-label { text-align: center; margin-top: -1px; margin-bottom: -1px; z-index: 100; border-left: 1px solid #ccc; border-right: 1px solid #ccc; color: #333; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #fff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #fff, #e6e6e6); background-image: -o-linear-gradient(top, #fff, #e6e6e6); background-image: linear-gradient(to bottom, #fff, #e6e6e6); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); border-color: #e6e6e6 #e6e6e6 #bfbfbf; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); *background-color: #e6e6e6; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .bootstrap-switch .bootstrap-switch-label:hover, .bootstrap-switch .bootstrap-switch-label:focus, .bootstrap-switch .bootstrap-switch-label:active, .bootstrap-switch .bootstrap-switch-label.active, .bootstrap-switch .bootstrap-switch-label.disabled, .bootstrap-switch .bootstrap-switch-label[disabled] { color: #333; background-color: #e6e6e6; *background-color: #d9d9d9; } .bootstrap-switch .bootstrap-switch-label:active, .bootstrap-switch .bootstrap-switch-label.active { background-color: #cccccc \9; } .bootstrap-switch span::before { content: "\200b"; } .bootstrap-switch .bootstrap-switch-handle-on { -webkit-border-top-left-radius: 4px; -moz-border-radius-topleft: 4px; border-top-left-radius: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomleft: 4px; border-bottom-left-radius: 4px; } .bootstrap-switch .bootstrap-switch-handle-off { -webkit-border-top-right-radius: 4px; -moz-border-radius-topright: 4px; border-top-right-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomright: 4px; border-bottom-right-radius: 4px; } .bootstrap-switch input[type='radio'], .bootstrap-switch input[type='checkbox'] { position: absolute !important; top: 0; left: 0; opacity: 0; filter: alpha(opacity=0); z-index: -1; visibility: hidden; } .bootstrap-switch input[type='radio'].form-control, .bootstrap-switch input[type='checkbox'].form-control { height: auto; } .bootstrap-switch.bootstrap-switch-mini { min-width: 71px; } .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label { padding: 3px 6px; font-size: 10px; line-height: 9px; } .bootstrap-switch.bootstrap-switch-small { min-width: 79px; } .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label { padding: 3px 6px; font-size: 12px; line-height: 18px; } .bootstrap-switch.bootstrap-switch-large { min-width: 120px; } .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label { padding: 9px 12px; font-size: 16px; line-height: normal; } .bootstrap-switch.bootstrap-switch-disabled, .bootstrap-switch.bootstrap-switch-readonly, .bootstrap-switch.bootstrap-switch-indeterminate { cursor: default !important; } .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label { opacity: 0.5; filter: alpha(opacity=50); cursor: default !important; } .bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container { -webkit-transition: margin-left 0.5s; -moz-transition: margin-left 0.5s; -o-transition: margin-left 0.5s; transition: margin-left 0.5s; } .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on { -webkit-border-top-left-radius: 0; -moz-border-radius-topleft: 0; border-top-left-radius: 0; -webkit-border-bottom-left-radius: 0; -moz-border-radius-bottomleft: 0; border-bottom-left-radius: 0; -webkit-border-top-right-radius: 4px; -moz-border-radius-topright: 4px; border-top-right-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomright: 4px; border-bottom-right-radius: 4px; } .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off { -webkit-border-top-right-radius: 0; -moz-border-radius-topright: 0; border-top-right-radius: 0; -webkit-border-bottom-right-radius: 0; -moz-border-radius-bottomright: 0; border-bottom-right-radius: 0; -webkit-border-top-left-radius: 4px; -moz-border-radius-topleft: 4px; border-top-left-radius: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomleft: 4px; border-bottom-left-radius: 4px; } .bootstrap-switch.bootstrap-switch-focused { border-color: rgba(82, 168, 236, 0.8); outline: 0; outline: thin dotted \9; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82, 168, 236, .6); -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82, 168, 236, .6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82, 168, 236, .6); } .bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label { -webkit-border-top-right-radius: 4px; -moz-border-radius-topright: 4px; border-top-right-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomright: 4px; border-bottom-right-radius: 4px; } .bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label { -webkit-border-top-left-radius: 4px; -moz-border-radius-topleft: 4px; border-top-left-radius: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomleft: 4px; border-bottom-left-radius: 4px; } ================================================ FILE: static/bootstrap/plugins/bootstrap-switch/css/bootstrap3/bootstrap-switch.css ================================================ /** * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. * * @version v3.3.4 * @homepage https://bttstrp.github.io/bootstrap-switch * @author Mattia Larentis (http://larentis.eu) * @license Apache-2.0 */ .bootstrap-switch { display: inline-block; direction: ltr; cursor: pointer; border-radius: 4px; border: 1px solid; border-color: #ccc; position: relative; text-align: left; overflow: hidden; line-height: 8px; z-index: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; vertical-align: middle; -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } .bootstrap-switch .bootstrap-switch-container { display: inline-block; top: 0; border-radius: 4px; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .bootstrap-switch .bootstrap-switch-handle-on, .bootstrap-switch .bootstrap-switch-handle-off, .bootstrap-switch .bootstrap-switch-label { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; cursor: pointer; display: table-cell; vertical-align: middle; padding: 6px 12px; font-size: 14px; line-height: 20px; } .bootstrap-switch .bootstrap-switch-handle-on, .bootstrap-switch .bootstrap-switch-handle-off { text-align: center; z-index: 1; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary { color: #fff; background: #337ab7; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info { color: #fff; background: #5bc0de; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success { color: #fff; background: #5cb85c; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning { background: #f0ad4e; color: #fff; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger { color: #fff; background: #d9534f; } .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default { color: #000; background: #eeeeee; } .bootstrap-switch .bootstrap-switch-label { text-align: center; margin-top: -1px; margin-bottom: -1px; z-index: 100; color: #333; background: #fff; } .bootstrap-switch span::before { content: "\200b"; } .bootstrap-switch .bootstrap-switch-handle-on { border-bottom-left-radius: 3px; border-top-left-radius: 3px; } .bootstrap-switch .bootstrap-switch-handle-off { border-bottom-right-radius: 3px; border-top-right-radius: 3px; } .bootstrap-switch input[type='radio'], .bootstrap-switch input[type='checkbox'] { position: absolute !important; top: 0; left: 0; margin: 0; z-index: -1; opacity: 0; filter: alpha(opacity=0); visibility: hidden; } .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label { padding: 1px 5px; font-size: 12px; line-height: 1.5; } .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label { padding: 5px 10px; font-size: 12px; line-height: 1.5; } .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label { padding: 6px 16px; font-size: 18px; line-height: 1.3333333; } .bootstrap-switch.bootstrap-switch-disabled, .bootstrap-switch.bootstrap-switch-readonly, .bootstrap-switch.bootstrap-switch-indeterminate { cursor: default !important; } .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on, .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off, .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label { opacity: 0.5; filter: alpha(opacity=50); cursor: default !important; } .bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container { -webkit-transition: margin-left 0.5s; -o-transition: margin-left 0.5s; transition: margin-left 0.5s; } .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on { border-bottom-left-radius: 0; border-top-left-radius: 0; border-bottom-right-radius: 3px; border-top-right-radius: 3px; } .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off { border-bottom-right-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 3px; border-top-left-radius: 3px; } .bootstrap-switch.bootstrap-switch-focused { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); } .bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label { border-bottom-right-radius: 3px; border-top-right-radius: 3px; } .bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label, .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label { border-bottom-left-radius: 3px; border-top-left-radius: 3px; } ================================================ FILE: static/bootstrap/plugins/bootstrap-switch/js/bootstrap-switch.js ================================================ /** * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. * * @version v3.3.4 * @homepage https://bttstrp.github.io/bootstrap-switch * @author Mattia Larentis (http://larentis.eu) * @license Apache-2.0 */ (function (global, factory) { if (typeof define === "function" && define.amd) { define(['jquery'], factory); } else if (typeof exports !== "undefined") { factory(require('jquery')); } else { var mod = { exports: {} }; factory(global.jquery); global.bootstrapSwitch = mod.exports; } })(this, function (_jquery) { 'use strict'; var _jquery2 = _interopRequireDefault(_jquery); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var $ = _jquery2.default || window.jQuery || window.$; var BootstrapSwitch = function () { function BootstrapSwitch(element) { var _this = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, BootstrapSwitch); this.$element = $(element); this.options = $.extend({}, $.fn.bootstrapSwitch.defaults, this._getElementOptions(), options); this.prevOptions = {}; this.$wrapper = $('
    ', { class: function _class() { var classes = []; classes.push(_this.options.state ? 'on' : 'off'); if (_this.options.size) { classes.push(_this.options.size); } if (_this.options.disabled) { classes.push('disabled'); } if (_this.options.readonly) { classes.push('readonly'); } if (_this.options.indeterminate) { classes.push('indeterminate'); } if (_this.options.inverse) { classes.push('inverse'); } if (_this.$element.attr('id')) { classes.push('id-' + _this.$element.attr('id')); } return classes.map(_this._getClass.bind(_this)).concat([_this.options.baseClass], _this._getClasses(_this.options.wrapperClass)).join(' '); } }); this.$container = $('
    ', { class: this._getClass('container') }); this.$on = $('', { html: this.options.onText, class: this._getClass('handle-on') + ' ' + this._getClass(this.options.onColor) }); this.$off = $('', { html: this.options.offText, class: this._getClass('handle-off') + ' ' + this._getClass(this.options.offColor) }); this.$label = $('', { html: this.options.labelText, class: this._getClass('label') }); this.$element.on('init.bootstrapSwitch', this.options.onInit.bind(this, element)); this.$element.on('switchChange.bootstrapSwitch', function () { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (_this.options.onSwitchChange.apply(element, args) === false) { if (_this.$element.is(':radio')) { $('[name="' + _this.$element.attr('name') + '"]').trigger('previousState.bootstrapSwitch', true); } else { _this.$element.trigger('previousState.bootstrapSwitch', true); } } }); this.$container = this.$element.wrap(this.$container).parent(); this.$wrapper = this.$container.wrap(this.$wrapper).parent(); this.$element.before(this.options.inverse ? this.$off : this.$on).before(this.$label).before(this.options.inverse ? this.$on : this.$off); if (this.options.indeterminate) { this.$element.prop('indeterminate', true); } this._init(); this._elementHandlers(); this._handleHandlers(); this._labelHandlers(); this._formHandler(); this._externalLabelHandler(); this.$element.trigger('init.bootstrapSwitch', this.options.state); } _createClass(BootstrapSwitch, [{ key: 'setPrevOptions', value: function setPrevOptions() { this.prevOptions = _extends({}, this.options); } }, { key: 'state', value: function state(value, skip) { if (typeof value === 'undefined') { return this.options.state; } if (this.options.disabled || this.options.readonly || this.options.state && !this.options.radioAllOff && this.$element.is(':radio')) { return this.$element; } if (this.$element.is(':radio')) { $('[name="' + this.$element.attr('name') + '"]').trigger('setPreviousOptions.bootstrapSwitch'); } else { this.$element.trigger('setPreviousOptions.bootstrapSwitch'); } if (this.options.indeterminate) { this.indeterminate(false); } this.$element.prop('checked', Boolean(value)).trigger('change.bootstrapSwitch', skip); return this.$element; } }, { key: 'toggleState', value: function toggleState(skip) { if (this.options.disabled || this.options.readonly) { return this.$element; } if (this.options.indeterminate) { this.indeterminate(false); return this.state(true); } else { return this.$element.prop('checked', !this.options.state).trigger('change.bootstrapSwitch', skip); } } }, { key: 'size', value: function size(value) { if (typeof value === 'undefined') { return this.options.size; } if (this.options.size != null) { this.$wrapper.removeClass(this._getClass(this.options.size)); } if (value) { this.$wrapper.addClass(this._getClass(value)); } this._width(); this._containerPosition(); this.options.size = value; return this.$element; } }, { key: 'animate', value: function animate(value) { if (typeof value === 'undefined') { return this.options.animate; } if (this.options.animate === Boolean(value)) { return this.$element; } return this.toggleAnimate(); } }, { key: 'toggleAnimate', value: function toggleAnimate() { this.options.animate = !this.options.animate; this.$wrapper.toggleClass(this._getClass('animate')); return this.$element; } }, { key: 'disabled', value: function disabled(value) { if (typeof value === 'undefined') { return this.options.disabled; } if (this.options.disabled === Boolean(value)) { return this.$element; } return this.toggleDisabled(); } }, { key: 'toggleDisabled', value: function toggleDisabled() { this.options.disabled = !this.options.disabled; this.$element.prop('disabled', this.options.disabled); this.$wrapper.toggleClass(this._getClass('disabled')); return this.$element; } }, { key: 'readonly', value: function readonly(value) { if (typeof value === 'undefined') { return this.options.readonly; } if (this.options.readonly === Boolean(value)) { return this.$element; } return this.toggleReadonly(); } }, { key: 'toggleReadonly', value: function toggleReadonly() { this.options.readonly = !this.options.readonly; this.$element.prop('readonly', this.options.readonly); this.$wrapper.toggleClass(this._getClass('readonly')); return this.$element; } }, { key: 'indeterminate', value: function indeterminate(value) { if (typeof value === 'undefined') { return this.options.indeterminate; } if (this.options.indeterminate === Boolean(value)) { return this.$element; } return this.toggleIndeterminate(); } }, { key: 'toggleIndeterminate', value: function toggleIndeterminate() { this.options.indeterminate = !this.options.indeterminate; this.$element.prop('indeterminate', this.options.indeterminate); this.$wrapper.toggleClass(this._getClass('indeterminate')); this._containerPosition(); return this.$element; } }, { key: 'inverse', value: function inverse(value) { if (typeof value === 'undefined') { return this.options.inverse; } if (this.options.inverse === Boolean(value)) { return this.$element; } return this.toggleInverse(); } }, { key: 'toggleInverse', value: function toggleInverse() { this.$wrapper.toggleClass(this._getClass('inverse')); var $on = this.$on.clone(true); var $off = this.$off.clone(true); this.$on.replaceWith($off); this.$off.replaceWith($on); this.$on = $off; this.$off = $on; this.options.inverse = !this.options.inverse; return this.$element; } }, { key: 'onColor', value: function onColor(value) { if (typeof value === 'undefined') { return this.options.onColor; } if (this.options.onColor) { this.$on.removeClass(this._getClass(this.options.onColor)); } this.$on.addClass(this._getClass(value)); this.options.onColor = value; return this.$element; } }, { key: 'offColor', value: function offColor(value) { if (typeof value === 'undefined') { return this.options.offColor; } if (this.options.offColor) { this.$off.removeClass(this._getClass(this.options.offColor)); } this.$off.addClass(this._getClass(value)); this.options.offColor = value; return this.$element; } }, { key: 'onText', value: function onText(value) { if (typeof value === 'undefined') { return this.options.onText; } this.$on.html(value); this._width(); this._containerPosition(); this.options.onText = value; return this.$element; } }, { key: 'offText', value: function offText(value) { if (typeof value === 'undefined') { return this.options.offText; } this.$off.html(value); this._width(); this._containerPosition(); this.options.offText = value; return this.$element; } }, { key: 'labelText', value: function labelText(value) { if (typeof value === 'undefined') { return this.options.labelText; } this.$label.html(value); this._width(); this.options.labelText = value; return this.$element; } }, { key: 'handleWidth', value: function handleWidth(value) { if (typeof value === 'undefined') { return this.options.handleWidth; } this.options.handleWidth = value; this._width(); this._containerPosition(); return this.$element; } }, { key: 'labelWidth', value: function labelWidth(value) { if (typeof value === 'undefined') { return this.options.labelWidth; } this.options.labelWidth = value; this._width(); this._containerPosition(); return this.$element; } }, { key: 'baseClass', value: function baseClass(value) { return this.options.baseClass; } }, { key: 'wrapperClass', value: function wrapperClass(value) { if (typeof value === 'undefined') { return this.options.wrapperClass; } if (!value) { value = $.fn.bootstrapSwitch.defaults.wrapperClass; } this.$wrapper.removeClass(this._getClasses(this.options.wrapperClass).join(' ')); this.$wrapper.addClass(this._getClasses(value).join(' ')); this.options.wrapperClass = value; return this.$element; } }, { key: 'radioAllOff', value: function radioAllOff(value) { if (typeof value === 'undefined') { return this.options.radioAllOff; } var val = Boolean(value); if (this.options.radioAllOff === val) { return this.$element; } this.options.radioAllOff = val; return this.$element; } }, { key: 'onInit', value: function onInit(value) { if (typeof value === 'undefined') { return this.options.onInit; } if (!value) { value = $.fn.bootstrapSwitch.defaults.onInit; } this.options.onInit = value; return this.$element; } }, { key: 'onSwitchChange', value: function onSwitchChange(value) { if (typeof value === 'undefined') { return this.options.onSwitchChange; } if (!value) { value = $.fn.bootstrapSwitch.defaults.onSwitchChange; } this.options.onSwitchChange = value; return this.$element; } }, { key: 'destroy', value: function destroy() { var $form = this.$element.closest('form'); if ($form.length) { $form.off('reset.bootstrapSwitch').removeData('bootstrap-switch'); } this.$container.children().not(this.$element).remove(); this.$element.unwrap().unwrap().off('.bootstrapSwitch').removeData('bootstrap-switch'); return this.$element; } }, { key: '_getElementOptions', value: function _getElementOptions() { return { state: this.$element.is(':checked'), size: this.$element.data('size'), animate: this.$element.data('animate'), disabled: this.$element.is(':disabled'), readonly: this.$element.is('[readonly]'), indeterminate: this.$element.data('indeterminate'), inverse: this.$element.data('inverse'), radioAllOff: this.$element.data('radio-all-off'), onColor: this.$element.data('on-color'), offColor: this.$element.data('off-color'), onText: this.$element.data('on-text'), offText: this.$element.data('off-text'), labelText: this.$element.data('label-text'), handleWidth: this.$element.data('handle-width'), labelWidth: this.$element.data('label-width'), baseClass: this.$element.data('base-class'), wrapperClass: this.$element.data('wrapper-class') }; } }, { key: '_width', value: function _width() { var _this2 = this; var $handles = this.$on.add(this.$off).add(this.$label).css('width', ''); var handleWidth = this.options.handleWidth === 'auto' ? Math.round(Math.max(this.$on.width(), this.$off.width())) : this.options.handleWidth; $handles.width(handleWidth); this.$label.width(function (index, width) { if (_this2.options.labelWidth !== 'auto') { return _this2.options.labelWidth; } if (width < handleWidth) { return handleWidth; } return width; }); this._handleWidth = this.$on.outerWidth(); this._labelWidth = this.$label.outerWidth(); this.$container.width(this._handleWidth * 2 + this._labelWidth); return this.$wrapper.width(this._handleWidth + this._labelWidth); } }, { key: '_containerPosition', value: function _containerPosition() { var _this3 = this; var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.options.state; var callback = arguments[1]; this.$container.css('margin-left', function () { var values = [0, '-' + _this3._handleWidth + 'px']; if (_this3.options.indeterminate) { return '-' + _this3._handleWidth / 2 + 'px'; } if (state) { if (_this3.options.inverse) { return values[1]; } else { return values[0]; } } else { if (_this3.options.inverse) { return values[0]; } else { return values[1]; } } }); } }, { key: '_init', value: function _init() { var _this4 = this; var init = function init() { _this4.setPrevOptions(); _this4._width(); _this4._containerPosition(); setTimeout(function () { if (_this4.options.animate) { return _this4.$wrapper.addClass(_this4._getClass('animate')); } }, 50); }; if (this.$wrapper.is(':visible')) { init(); return; } var initInterval = window.setInterval(function () { if (_this4.$wrapper.is(':visible')) { init(); return window.clearInterval(initInterval); } }, 50); } }, { key: '_elementHandlers', value: function _elementHandlers() { var _this5 = this; return this.$element.on({ 'setPreviousOptions.bootstrapSwitch': this.setPrevOptions.bind(this), 'previousState.bootstrapSwitch': function previousStateBootstrapSwitch() { _this5.options = _this5.prevOptions; if (_this5.options.indeterminate) { _this5.$wrapper.addClass(_this5._getClass('indeterminate')); } _this5.$element.prop('checked', _this5.options.state).trigger('change.bootstrapSwitch', true); }, 'change.bootstrapSwitch': function changeBootstrapSwitch(event, skip) { event.preventDefault(); event.stopImmediatePropagation(); var state = _this5.$element.is(':checked'); _this5._containerPosition(state); if (state === _this5.options.state) { return; } _this5.options.state = state; _this5.$wrapper.toggleClass(_this5._getClass('off')).toggleClass(_this5._getClass('on')); if (!skip) { if (_this5.$element.is(':radio')) { $('[name="' + _this5.$element.attr('name') + '"]').not(_this5.$element).prop('checked', false).trigger('change.bootstrapSwitch', true); } _this5.$element.trigger('switchChange.bootstrapSwitch', [state]); } }, 'focus.bootstrapSwitch': function focusBootstrapSwitch(event) { event.preventDefault(); _this5.$wrapper.addClass(_this5._getClass('focused')); }, 'blur.bootstrapSwitch': function blurBootstrapSwitch(event) { event.preventDefault(); _this5.$wrapper.removeClass(_this5._getClass('focused')); }, 'keydown.bootstrapSwitch': function keydownBootstrapSwitch(event) { if (!event.which || _this5.options.disabled || _this5.options.readonly) { return; } if (event.which === 37 || event.which === 39) { event.preventDefault(); event.stopImmediatePropagation(); _this5.state(event.which === 39); } } }); } }, { key: '_handleHandlers', value: function _handleHandlers() { var _this6 = this; this.$on.on('click.bootstrapSwitch', function (event) { event.preventDefault(); event.stopPropagation(); _this6.state(false); return _this6.$element.trigger('focus.bootstrapSwitch'); }); return this.$off.on('click.bootstrapSwitch', function (event) { event.preventDefault(); event.stopPropagation(); _this6.state(true); return _this6.$element.trigger('focus.bootstrapSwitch'); }); } }, { key: '_labelHandlers', value: function _labelHandlers() { var _this7 = this; var handlers = { click: function click(event) { event.stopPropagation(); }, 'mousedown.bootstrapSwitch touchstart.bootstrapSwitch': function mousedownBootstrapSwitchTouchstartBootstrapSwitch(event) { if (_this7._dragStart || _this7.options.disabled || _this7.options.readonly) { return; } event.preventDefault(); event.stopPropagation(); _this7._dragStart = (event.pageX || event.originalEvent.touches[0].pageX) - parseInt(_this7.$container.css('margin-left'), 10); if (_this7.options.animate) { _this7.$wrapper.removeClass(_this7._getClass('animate')); } _this7.$element.trigger('focus.bootstrapSwitch'); }, 'mousemove.bootstrapSwitch touchmove.bootstrapSwitch': function mousemoveBootstrapSwitchTouchmoveBootstrapSwitch(event) { if (_this7._dragStart == null) { return; } var difference = (event.pageX || event.originalEvent.touches[0].pageX) - _this7._dragStart; event.preventDefault(); if (difference < -_this7._handleWidth || difference > 0) { return; } _this7._dragEnd = difference; _this7.$container.css('margin-left', _this7._dragEnd + 'px'); }, 'mouseup.bootstrapSwitch touchend.bootstrapSwitch': function mouseupBootstrapSwitchTouchendBootstrapSwitch(event) { if (!_this7._dragStart) { return; } event.preventDefault(); if (_this7.options.animate) { _this7.$wrapper.addClass(_this7._getClass('animate')); } if (_this7._dragEnd) { var state = _this7._dragEnd > -(_this7._handleWidth / 2); _this7._dragEnd = false; _this7.state(_this7.options.inverse ? !state : state); } else { _this7.state(!_this7.options.state); } _this7._dragStart = false; }, 'mouseleave.bootstrapSwitch': function mouseleaveBootstrapSwitch() { _this7.$label.trigger('mouseup.bootstrapSwitch'); } }; this.$label.on(handlers); } }, { key: '_externalLabelHandler', value: function _externalLabelHandler() { var _this8 = this; var $externalLabel = this.$element.closest('label'); $externalLabel.on('click', function (event) { event.preventDefault(); event.stopImmediatePropagation(); if (event.target === $externalLabel[0]) { _this8.toggleState(); } }); } }, { key: '_formHandler', value: function _formHandler() { var $form = this.$element.closest('form'); if ($form.data('bootstrap-switch')) { return; } $form.on('reset.bootstrapSwitch', function () { window.setTimeout(function () { $form.find('input').filter(function () { return $(this).data('bootstrap-switch'); }).each(function () { return $(this).bootstrapSwitch('state', this.checked); }); }, 1); }).data('bootstrap-switch', true); } }, { key: '_getClass', value: function _getClass(name) { return this.options.baseClass + '-' + name; } }, { key: '_getClasses', value: function _getClasses(classes) { if (!$.isArray(classes)) { return [this._getClass(classes)]; } return classes.map(this._getClass.bind(this)); } }]); return BootstrapSwitch; }(); $.fn.bootstrapSwitch = function (option) { for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } function reducer(ret, next) { var $this = $(next); var existingData = $this.data('bootstrap-switch'); var data = existingData || new BootstrapSwitch(next, option); if (!existingData) { $this.data('bootstrap-switch', data); } if (typeof option === 'string') { return data[option].apply(data, args); } return ret; } return Array.prototype.reduce.call(this, reducer, this); }; $.fn.bootstrapSwitch.Constructor = BootstrapSwitch; $.fn.bootstrapSwitch.defaults = { state: true, size: null, animate: true, disabled: false, readonly: false, indeterminate: false, inverse: false, radioAllOff: false, onColor: 'primary', offColor: 'default', onText: 'ON', offText: 'OFF', labelText: ' ', handleWidth: 'auto', labelWidth: 'auto', baseClass: 'bootstrap-switch', wrapperClass: 'wrapper', onInit: function onInit() {}, onSwitchChange: function onSwitchChange() {} }; }); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/bootstrap-wysiwyg.js ================================================ /* http://github.com/mindmup/bootstrap-wysiwyg */ /*global jQuery, $, FileReader*/ /*jslint browser:true*/ (function ($) { 'use strict'; var readFileIntoDataUrl = function (fileInfo) { var loader = $.Deferred(), fReader = new FileReader(); fReader.onload = function (e) { loader.resolve(e.target.result); }; fReader.onerror = loader.reject; fReader.onprogress = loader.notify; fReader.readAsDataURL(fileInfo); return loader.promise(); }; $.fn.cleanHtml = function () { var html = $(this).html(); return html && html.replace(/(
    |\s|

    <\/div>| )*$/, ''); }; $.fn.wysiwyg = function (userOptions) { var editor = this, selectedRange, options, toolbarBtnSelector, updateToolbar = function () { if (options.activeToolbarClass) { var selection = window.getSelection(); try { var tag = 'formatBlock ' + selection.focusNode.parentNode.nodeName.toLowerCase(); }catch (e){ console.log(e); tag = ''; } $(options.toolbarSelector).find(toolbarBtnSelector).each(function () { var command = $(this).data(options.commandRole); if (document.queryCommandState(command) || tag === command) { $(this).addClass(options.activeToolbarClass); } else { $(this).removeClass(options.activeToolbarClass); } }); } }, execCommand = function (commandWithArgs, valueArg) { var commandArr = commandWithArgs.split(' '), command = commandArr.shift(), args = commandArr.join(' ') + (valueArg || ''); if(command === 'formatBlock'){ var selection = window.getSelection(); if(selection.focusNode.parentNode.nodeName.toLowerCase() === args){ args = '

    '; }else{ args = '<' + args + '>'; } }else if(command === 'enterAction'){ } document.execCommand(command, 0, args); updateToolbar(); editor.change && editor.change(); }, insertEmpty = function ($selectionElem) { insertHtml('\r\n'); return true; }, codeHandler = function () { var selection = window.getSelection(); try{ var nodeName = selection.parentNode.nodeName; console.log(nodeName) if(nodeName !== 'CODE' && nodeName !== 'PRE'){ } if (!document.queryCommandSupported('insertHTML')) { } }catch (e){ console.log(e) } }, enterKeyHandle = function (e) { var selection = getCurrentRange(); try { var nodeName = selection.commonAncestorContainer.parentNode.nodeName; if(nodeName === 'CODE' || nodeName === 'PRE'){ return insertEmpty(selection.parentNode); }else if(nodeName === "DIV" || nodeName === "P"){ return insertHtml('
    '); } console.log(nodeName); }catch (e){ console.log(selection) console.log("enterKeyHandle:" + e); } }, bindHotkeys = function (hotKeys) { $.each(hotKeys, function (hotkey, command) { editor.keydown(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); if(hotkey === 'return'){ return enterKeyHandle(e); } execCommand(command); } }).keyup(hotkey, function (e) { if (editor.attr('contenteditable') && editor.is(':visible')) { e.preventDefault(); e.stopPropagation(); } }); }); }, getCurrentRange = function () { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } }, saveSelection = function () { selectedRange = getCurrentRange(); }, restoreSelection = function () { var selection = window.getSelection(); if (selectedRange) { try { selection.removeAllRanges(); } catch (ex) { document.body.createTextRange().select(); document.selection.empty(); } selection.addRange(selectedRange); } }, insertFiles = function (files) { editor.focus(); $.each(files, function (idx, fileInfo) { if (/^image\//.test(fileInfo.type)) { $.when(readFileIntoDataUrl(fileInfo)).done(function (dataUrl) { execCommand('insertimage', dataUrl); }).fail(function (e) { options.fileUploadError("file-reader", e); }); } else { options.fileUploadError("unsupported-file-type", fileInfo.type); } }); }, markSelection = function (input, color) { restoreSelection(); if (document.queryCommandSupported('hiliteColor')) { document.execCommand('hiliteColor', 0, color || 'transparent'); } saveSelection(); input.data(options.selectionMarker, color); }, bindToolbar = function (toolbar, options) { toolbar.find(toolbarBtnSelector).click(function () { restoreSelection(); editor.focus(); execCommand($(this).data(options.commandRole)); saveSelection(); }); toolbar.find('[data-toggle=dropdown]').click(restoreSelection); toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () { var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */ this.value = ''; restoreSelection(); if (newValue) { editor.focus(); execCommand($(this).data(options.commandRole), newValue); } saveSelection(); }).on('focus', function () { var input = $(this); if (!input.data(options.selectionMarker)) { markSelection(input, options.selectionColor); input.focus(); } }).on('blur', function () { var input = $(this); if (input.data(options.selectionMarker)) { markSelection(input, false); } }); toolbar.find('input[type=file][data-' + options.commandRole + ']').change(function () { restoreSelection(); if (this.type === 'file' && this.files && this.files.length > 0) { insertFiles(this.files); } saveSelection(); this.value = ''; }); }, initFileDrops = function () { editor.on('dragenter dragover', false) .on('drop', function (e) { var dataTransfer = e.originalEvent.dataTransfer; e.stopPropagation(); e.preventDefault(); if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { insertFiles(dataTransfer.files); } }); }, insertHtml = function (html) { if(!document.queryCommandSupported('insertHTML')){ var range = window.getSelection().getRangeAt(0); if (range.insertNode) { // IE range.deleteContents(); range.insertNode($(args)[0]); updateToolbar(); editor.change && editor.change(); } else if (range.pasteHTML) { // IE <= 10 range.pasteHTML(args); updateToolbar(); editor.change && editor.change(); } }else{ console.log(html) execCommand('insertHTML',html); } }; options = $.extend({}, $.fn.wysiwyg.defaults, userOptions); toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + ']'; bindHotkeys(options.hotKeys); if (options.dragAndDropImages) { initFileDrops(); } bindToolbar($(options.toolbarSelector), options); editor.attr('contenteditable', true) .on('mouseup keyup mouseout', function () { saveSelection(); updateToolbar(); }); $(window).bind('touchend', function (e) { var isInside = (editor.is(e.target) || editor.has(e.target).length > 0), currentRange = getCurrentRange(), clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); if (!clear || isInside) { saveSelection(); updateToolbar(); } }); this.insertLink = function (linkUrl,linkTitle) { restoreSelection(); editor.focus(); var args = ''+linkTitle+''; insertHtml(args); saveSelection(); }; this.insertHtml = insertHtml; return this; }; $.fn.wysiwyg.defaults = { hotKeys: { 'ctrl+b meta+b': 'bold', 'ctrl+i meta+i': 'italic', 'ctrl+u meta+u': 'underline', 'ctrl+z meta+z': 'undo', 'ctrl+y meta+y meta+shift+z': 'redo', 'ctrl+l meta+l': 'justifyleft', 'ctrl+r meta+r': 'justifyright', 'ctrl+e meta+e': 'justifycenter', 'ctrl+j meta+j': 'justifyfull', 'shift+tab': 'outdent', 'tab': 'indent', 'return':'enterAction' }, toolbarSelector: '[data-role=editor-toolbar]', commandRole: 'edit', activeToolbarClass: 'btn-info', selectionMarker: 'edit-focus-marker', selectionColor: 'darkgrey', dragAndDropImages: true, fileUploadError: function (reason, detail) { console.log("File upload error", reason, detail); } }; }(window.jQuery)); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-apollo.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\n\r]*/,null,"#"],["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/, null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[ES]?BANK=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[!-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["apollo","agc","aea"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-basic.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["str",/^"(?:[^\n\r"\\]|\\.)*(?:"|$)/,a,'"'],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^REM[^\n\r]*/,a],["kwd",/^\b(?:AND|CLOSE|CLR|CMD|CONT|DATA|DEF ?FN|DIM|END|FOR|GET|GOSUB|GOTO|IF|INPUT|LET|LIST|LOAD|NEW|NEXT|NOT|ON|OPEN|OR|POKE|PRINT|READ|RESTORE|RETURN|RUN|SAVE|STEP|STOP|SYS|THEN|TO|VERIFY|WAIT)\b/,a],["pln",/^[a-z][^\W_]?(?:\$|%)?/i,a],["lit",/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i,a,"0123456789"],["pun", /^.[^\s\w"$%.]*/,a]]),["basic","cbm"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-clj.js ================================================ /* Copyright (C) 2011 Google Inc. 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. */ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["opn",/^[([{]+/,a,"([{"],["clo",/^[)\]}]+/,a,")]}"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/,a], ["typ",/^:[\dA-Za-z-]+/]]),["clj"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-css.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n\u000c"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]+)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//], ["com",/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}\b/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-dart.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["com",/^#!.*/],["kwd",/^\b(?:import|library|part of|part|as|show|hide)\b/i],["com",/^\/\/.*/],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["kwd",/^\b(?:class|interface)\b/i],["kwd",/^\b(?:assert|break|case|catch|continue|default|do|else|finally|for|if|in|is|new|return|super|switch|this|throw|try|while)\b/i],["kwd",/^\b(?:abstract|const|extends|factory|final|get|implements|native|operator|set|static|typedef|var)\b/i], ["typ",/^\b(?:bool|double|dynamic|int|num|object|string|void)\b/i],["kwd",/^\b(?:false|null|true)\b/i],["str",/^r?'''[\S\s]*?[^\\]'''/],["str",/^r?"""[\S\s]*?[^\\]"""/],["str",/^r?'('|[^\n\f\r]*?[^\\]')/],["str",/^r?"("|[^\n\f\r]*?[^\\]")/],["pln",/^[$_a-z]\w*/i],["pun",/^[!%&*+/:<-?^|~-]/],["lit",/^\b0x[\da-f]+/i],["lit",/^\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i],["lit",/^\b\.\d+(?:e[+-]?\d+)?/i],["pun",/^[(),.;[\]{}]/]]), ["dart"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-erlang.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["lit",/^[a-z]\w*/],["lit",/^'(?:[^\n\f\r'\\]|\\[^&])+'?/,null,"'"],["lit",/^\?[^\t\n ({]+/,null,"?"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^%[^\n]*/],["kwd",/^(?:module|attributes|do|let|in|letrec|apply|call|primop|case|of|end|when|fun|try|catch|receive|after|char|integer|float,atom,string,var)\b/], ["kwd",/^-[_a-z]+/],["typ",/^[A-Z_]\w*/],["pun",/^[,.;]/]]),["erlang","erl"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-go.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-hs.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/, null],["pln",/^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/],["pun",/^[^\d\t-\r "'A-Za-z]+/]]),["hs"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-lisp.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(+/,a,"("],["clo",/^\)+/,a,")"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,a], ["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["cl","el","lisp","lsp","scm","ss","rkt"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-llvm.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^!?"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["com",/^;[^\n\r]*/,null,";"]],[["pln",/^[!%@](?:[$\-.A-Z_a-z][\w$\-.]*|\d+)/],["kwd",/^[^\W\d]\w*/,null],["lit",/^\d+\.\d+/],["lit",/^(?:\d+|0[Xx][\dA-Fa-f]+)/],["pun",/^[(-*,:<->[\]{}]|\.\.\.$/]]),["llvm","ll"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-lua.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\S\s]*?(?:]\1]|$)|[^\n\r]*)/],["str",/^\[(=*)\[[\S\s]*?(?:]\1]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i], ["pln",/^[_a-z]\w*/i],["pun",/^[^\w\t\n\r \xa0][^\w\t\n\r "'+=\xa0-]*/]]),["lua"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-matlab.js ================================================ var a=null,b=window.PR,c=[[b.PR_PLAIN,/^[\t-\r \xa0]+/,a," \t\r\n\u000b\u000c\u00a0"],[b.PR_COMMENT,/^%{[^%]*%+(?:[^%}][^%]*%+)*}/,a],[b.PR_COMMENT,/^%[^\n\r]*/,a,"%"],["syscmd",/^![^\n\r]*/,a,"!"]],d=[["linecont",/^\.\.\.\s*[\n\r]/,a],["err",/^\?\?\? [^\n\r]*/,a],["wrn",/^Warning: [^\n\r]*/,a],["codeoutput",/^>>\s+/,a],["codeoutput",/^octave:\d+>\s+/,a],["lang-matlab-operators",/^((?:[A-Za-z]\w*(?:\.[A-Za-z]\w*)*|[).\]}])')/,a],["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)(?!')/,a], [b.PR_STRING,/^'(?:[^']|'')*'/,a],[b.PR_LITERAL,/^[+-]?\.?\d+(?:\.\d*)?(?:[Ee][+-]?\d+)?[ij]?/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a]],e=[["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a],["transpose",/^'/,a]]; b.registerLangHandler(b.createSimpleLexer([],[[b.PR_KEYWORD,/^\b(?:break|case|catch|classdef|continue|else|elseif|end|for|function|global|if|otherwise|parfor|persistent|return|spmd|switch|try|while)\b/,a],["const",/^\b(?:true|false|inf|Inf|nan|NaN|eps|pi|ans|nargin|nargout|varargin|varargout)\b/,a],[b.PR_TYPE,/^\b(?:cell|struct|char|double|single|logical|u?int(?:8|16|32|64)|sparse)\b/,a],["fun",/^\b(?:abs|accumarray|acos(?:d|h)?|acot(?:d|h)?|acsc(?:d|h)?|actxcontrol(?:list|select)?|actxGetRunningServer|actxserver|addlistener|addpath|addpref|addtodate|airy|align|alim|all|allchild|alpha|alphamap|amd|ancestor|and|angle|annotation|any|area|arrayfun|asec(?:d|h)?|asin(?:d|h)?|assert|assignin|atan[2dh]?|audiodevinfo|audioplayer|audiorecorder|aufinfo|auread|autumn|auwrite|avifile|aviinfo|aviread|axes|axis|balance|bar(?:3|3h|h)?|base2dec|beep|BeginInvoke|bench|bessel[h-ky]|beta|betainc|betaincinv|betaln|bicg|bicgstab|bicgstabl|bin2dec|bitand|bitcmp|bitget|bitmax|bitnot|bitor|bitset|bitshift|bitxor|blanks|blkdiag|bone|box|brighten|brush|bsxfun|builddocsearchdb|builtin|bvp4c|bvp5c|bvpget|bvpinit|bvpset|bvpxtend|calendar|calllib|callSoapService|camdolly|cameratoolbar|camlight|camlookat|camorbit|campan|campos|camproj|camroll|camtarget|camup|camva|camzoom|cart2pol|cart2sph|cast|cat|caxis|cd|cdf2rdf|cdfepoch|cdfinfo|cdflib(?:.(?:close|closeVar|computeEpoch|computeEpoch16|create|createAttr|createVar|delete|deleteAttr|deleteAttrEntry|deleteAttrgEntry|deleteVar|deleteVarRecords|epoch16Breakdown|epochBreakdown|getAttrEntry|getAttrgEntry|getAttrMaxEntry|getAttrMaxgEntry|getAttrName|getAttrNum|getAttrScope|getCacheSize|getChecksum|getCompression|getCompressionCacheSize|getConstantNames|getConstantValue|getCopyright|getFileBackward|getFormat|getLibraryCopyright|getLibraryVersion|getMajority|getName|getNumAttrEntries|getNumAttrgEntries|getNumAttributes|getNumgAttributes|getReadOnlyMode|getStageCacheSize|getValidate|getVarAllocRecords|getVarBlockingFactor|getVarCacheSize|getVarCompression|getVarData|getVarMaxAllocRecNum|getVarMaxWrittenRecNum|getVarName|getVarNum|getVarNumRecsWritten|getVarPadValue|getVarRecordData|getVarReservePercent|getVarsMaxWrittenRecNum|getVarSparseRecords|getVersion|hyperGetVarData|hyperPutVarData|inquire|inquireAttr|inquireAttrEntry|inquireAttrgEntry|inquireVar|open|putAttrEntry|putAttrgEntry|putVarData|putVarRecordData|renameAttr|renameVar|setCacheSize|setChecksum|setCompression|setCompressionCacheSize|setFileBackward|setFormat|setMajority|setReadOnlyMode|setStageCacheSize|setValidate|setVarAllocBlockRecords|setVarBlockingFactor|setVarCacheSize|setVarCompression|setVarInitialRecs|setVarPadValue|SetVarReservePercent|setVarsCacheSize|setVarSparseRecords))?|cdfread|cdfwrite|ceil|cell2mat|cell2struct|celldisp|cellfun|cellplot|cellstr|cgs|checkcode|checkin|checkout|chol|cholinc|cholupdate|circshift|cla|clabel|class|clc|clear|clearvars|clf|clipboard|clock|close|closereq|cmopts|cmpermute|cmunique|colamd|colon|colorbar|colordef|colormap|colormapeditor|colperm|Combine|comet|comet3|commandhistory|commandwindow|compan|compass|complex|computer|cond|condeig|condest|coneplot|conj|containers.Map|contour(?:[3cf]|slice)?|contrast|conv|conv2|convhull|convhulln|convn|cool|copper|copyfile|copyobj|corrcoef|cos(?:d|h)?|cot(?:d|h)?|cov|cplxpair|cputime|createClassFromWsdl|createSoapMessage|cross|csc(?:d|h)?|csvread|csvwrite|ctranspose|cumprod|cumsum|cumtrapz|curl|customverctrl|cylinder|daqread|daspect|datacursormode|datatipinfo|date|datenum|datestr|datetick|datevec|dbclear|dbcont|dbdown|dblquad|dbmex|dbquit|dbstack|dbstatus|dbstep|dbstop|dbtype|dbup|dde23|ddeget|ddesd|ddeset|deal|deblank|dec2base|dec2bin|dec2hex|decic|deconv|del2|delaunay|delaunay3|delaunayn|DelaunayTri|delete|demo|depdir|depfun|det|detrend|deval|diag|dialog|diary|diff|diffuse|dir|disp|display|dither|divergence|dlmread|dlmwrite|dmperm|doc|docsearch|dos|dot|dragrect|drawnow|dsearch|dsearchn|dynamicprops|echo|echodemo|edit|eig|eigs|ellipj|ellipke|ellipsoid|empty|enableNETfromNetworkDrive|enableservice|EndInvoke|enumeration|eomday|eq|erf|erfc|erfcinv|erfcx|erfinv|error|errorbar|errordlg|etime|etree|etreeplot|eval|evalc|evalin|event.(?:EventData|listener|PropertyEvent|proplistener)|exifread|exist|exit|exp|expint|expm|expm1|export2wsdlg|eye|ezcontour|ezcontourf|ezmesh|ezmeshc|ezplot|ezplot3|ezpolar|ezsurf|ezsurfc|factor|factorial|fclose|feather|feature|feof|ferror|feval|fft|fft2|fftn|fftshift|fftw|fgetl|fgets|fieldnames|figure|figurepalette|fileattrib|filebrowser|filemarker|fileparts|fileread|filesep|fill|fill3|filter|filter2|find|findall|findfigs|findobj|findstr|finish|fitsdisp|fitsinfo|fitsread|fitswrite|fix|flag|flipdim|fliplr|flipud|floor|flow|fminbnd|fminsearch|fopen|format|fplot|fprintf|frame2im|fread|freqspace|frewind|fscanf|fseek|ftell|FTP|full|fullfile|func2str|functions|funm|fwrite|fzero|gallery|gamma|gammainc|gammaincinv|gammaln|gca|gcbf|gcbo|gcd|gcf|gco|ge|genpath|genvarname|get|getappdata|getenv|getfield|getframe|getpixelposition|getpref|ginput|gmres|gplot|grabcode|gradient|gray|graymon|grid|griddata(?:3|n)?|griddedInterpolant|gsvd|gt|gtext|guidata|guide|guihandles|gunzip|gzip|h5create|h5disp|h5info|h5read|h5readatt|h5write|h5writeatt|hadamard|handle|hankel|hdf|hdf5|hdf5info|hdf5read|hdf5write|hdfinfo|hdfread|hdftool|help|helpbrowser|helpdesk|helpdlg|helpwin|hess|hex2dec|hex2num|hgexport|hggroup|hgload|hgsave|hgsetget|hgtransform|hidden|hilb|hist|histc|hold|home|horzcat|hostid|hot|hsv|hsv2rgb|hypot|ichol|idivide|ifft|ifft2|ifftn|ifftshift|ilu|im2frame|im2java|imag|image|imagesc|imapprox|imfinfo|imformats|import|importdata|imread|imwrite|ind2rgb|ind2sub|inferiorto|info|inline|inmem|inpolygon|input|inputdlg|inputname|inputParser|inspect|instrcallback|instrfind|instrfindall|int2str|integral(?:2|3)?|interp(?:1|1q|2|3|ft|n)|interpstreamspeed|intersect|intmax|intmin|inv|invhilb|ipermute|isa|isappdata|iscell|iscellstr|ischar|iscolumn|isdir|isempty|isequal|isequaln|isequalwithequalnans|isfield|isfinite|isfloat|isglobal|ishandle|ishghandle|ishold|isinf|isinteger|isjava|iskeyword|isletter|islogical|ismac|ismatrix|ismember|ismethod|isnan|isnumeric|isobject|isocaps|isocolors|isonormals|isosurface|ispc|ispref|isprime|isprop|isreal|isrow|isscalar|issorted|isspace|issparse|isstr|isstrprop|isstruct|isstudent|isunix|isvarname|isvector|javaaddpath|javaArray|javachk|javaclasspath|javacomponent|javaMethod|javaMethodEDT|javaObject|javaObjectEDT|javarmpath|jet|keyboard|kron|lasterr|lasterror|lastwarn|lcm|ldivide|ldl|le|legend|legendre|length|libfunctions|libfunctionsview|libisloaded|libpointer|libstruct|license|light|lightangle|lighting|lin2mu|line|lines|linkaxes|linkdata|linkprop|linsolve|linspace|listdlg|listfonts|load|loadlibrary|loadobj|log|log10|log1p|log2|loglog|logm|logspace|lookfor|lower|ls|lscov|lsqnonneg|lsqr|lt|lu|luinc|magic|makehgtform|mat2cell|mat2str|material|matfile|matlab.io.MatFile|matlab.mixin.(?:Copyable|Heterogeneous(?:.getDefaultScalarElement)?)|matlabrc|matlabroot|max|maxNumCompThreads|mean|median|membrane|memmapfile|memory|menu|mesh|meshc|meshgrid|meshz|meta.(?:class(?:.fromName)?|DynamicProperty|EnumeratedValue|event|MetaData|method|package(?:.(?:fromName|getAllPackages))?|property)|metaclass|methods|methodsview|mex(?:.getCompilerConfigurations)?|MException|mexext|mfilename|min|minres|minus|mislocked|mkdir|mkpp|mldivide|mlint|mlintrpt|mlock|mmfileinfo|mmreader|mod|mode|more|move|movefile|movegui|movie|movie2avi|mpower|mrdivide|msgbox|mtimes|mu2lin|multibandread|multibandwrite|munlock|namelengthmax|nargchk|narginchk|nargoutchk|native2unicode|nccreate|ncdisp|nchoosek|ncinfo|ncread|ncreadatt|ncwrite|ncwriteatt|ncwriteschema|ndgrid|ndims|ne|NET(?:.(?:addAssembly|Assembly|convertArray|createArray|createGeneric|disableAutoRelease|enableAutoRelease|GenericClass|invokeGenericMethod|NetException|setStaticProperty))?|netcdf.(?:abort|close|copyAtt|create|defDim|defGrp|defVar|defVarChunking|defVarDeflate|defVarFill|defVarFletcher32|delAtt|endDef|getAtt|getChunkCache|getConstant|getConstantNames|getVar|inq|inqAtt|inqAttID|inqAttName|inqDim|inqDimID|inqDimIDs|inqFormat|inqGrpName|inqGrpNameFull|inqGrpParent|inqGrps|inqLibVers|inqNcid|inqUnlimDims|inqVar|inqVarChunking|inqVarDeflate|inqVarFill|inqVarFletcher32|inqVarID|inqVarIDs|open|putAtt|putVar|reDef|renameAtt|renameDim|renameVar|setChunkCache|setDefaultFormat|setFill|sync)|newplot|nextpow2|nnz|noanimate|nonzeros|norm|normest|not|notebook|now|nthroot|null|num2cell|num2hex|num2str|numel|nzmax|ode(?:113|15i|15s|23|23s|23t|23tb|45)|odeget|odeset|odextend|onCleanup|ones|open|openfig|opengl|openvar|optimget|optimset|or|ordeig|orderfields|ordqz|ordschur|orient|orth|pack|padecoef|pagesetupdlg|pan|pareto|parseSoapResponse|pascal|patch|path|path2rc|pathsep|pathtool|pause|pbaspect|pcg|pchip|pcode|pcolor|pdepe|pdeval|peaks|perl|perms|permute|pie|pink|pinv|planerot|playshow|plot|plot3|plotbrowser|plotedit|plotmatrix|plottools|plotyy|plus|pol2cart|polar|poly|polyarea|polyder|polyeig|polyfit|polyint|polyval|polyvalm|pow2|power|ppval|prefdir|preferences|primes|print|printdlg|printopt|printpreview|prod|profile|profsave|propedit|propertyeditor|psi|publish|PutCharArray|PutFullMatrix|PutWorkspaceData|pwd|qhull|qmr|qr|qrdelete|qrinsert|qrupdate|quad|quad2d|quadgk|quadl|quadv|questdlg|quit|quiver|quiver3|qz|rand|randi|randn|randperm|RandStream(?:.(?:create|getDefaultStream|getGlobalStream|list|setDefaultStream|setGlobalStream))?|rank|rat|rats|rbbox|rcond|rdivide|readasync|real|reallog|realmax|realmin|realpow|realsqrt|record|rectangle|rectint|recycle|reducepatch|reducevolume|refresh|refreshdata|regexp|regexpi|regexprep|regexptranslate|rehash|rem|Remove|RemoveAll|repmat|reset|reshape|residue|restoredefaultpath|rethrow|rgb2hsv|rgb2ind|rgbplot|ribbon|rmappdata|rmdir|rmfield|rmpath|rmpref|rng|roots|rose|rosser|rot90|rotate|rotate3d|round|rref|rsf2csf|run|save|saveas|saveobj|savepath|scatter|scatter3|schur|sec|secd|sech|selectmoveresize|semilogx|semilogy|sendmail|serial|set|setappdata|setdiff|setenv|setfield|setpixelposition|setpref|setstr|setxor|shading|shg|shiftdim|showplottool|shrinkfaces|sign|sin(?:d|h)?|size|slice|smooth3|snapnow|sort|sortrows|sound|soundsc|spalloc|spaugment|spconvert|spdiags|specular|speye|spfun|sph2cart|sphere|spinmap|spline|spones|spparms|sprand|sprandn|sprandsym|sprank|spring|sprintf|spy|sqrt|sqrtm|squeeze|ss2tf|sscanf|stairs|startup|std|stem|stem3|stopasync|str2double|str2func|str2mat|str2num|strcat|strcmp|strcmpi|stream2|stream3|streamline|streamparticles|streamribbon|streamslice|streamtube|strfind|strjust|strmatch|strncmp|strncmpi|strread|strrep|strtok|strtrim|struct2cell|structfun|strvcat|sub2ind|subplot|subsasgn|subsindex|subspace|subsref|substruct|subvolume|sum|summer|superclasses|superiorto|support|surf|surf2patch|surface|surfc|surfl|surfnorm|svd|svds|swapbytes|symamd|symbfact|symmlq|symrcm|symvar|system|tan(?:d|h)?|tar|tempdir|tempname|tetramesh|texlabel|text|textread|textscan|textwrap|tfqmr|throw|tic|Tiff(?:.(?:getTagNames|getVersion))?|timer|timerfind|timerfindall|times|timeseries|title|toc|todatenum|toeplitz|toolboxdir|trace|transpose|trapz|treelayout|treeplot|tril|trimesh|triplequad|triplot|TriRep|TriScatteredInterp|trisurf|triu|tscollection|tsearch|tsearchn|tstool|type|typecast|uibuttongroup|uicontextmenu|uicontrol|uigetdir|uigetfile|uigetpref|uiimport|uimenu|uiopen|uipanel|uipushtool|uiputfile|uiresume|uisave|uisetcolor|uisetfont|uisetpref|uistack|uitable|uitoggletool|uitoolbar|uiwait|uminus|undocheckout|unicode2native|union|unique|unix|unloadlibrary|unmesh|unmkpp|untar|unwrap|unzip|uplus|upper|urlread|urlwrite|usejava|userpath|validateattributes|validatestring|vander|var|vectorize|ver|verctrl|verLessThan|version|vertcat|VideoReader(?:.isPlatformSupported)?|VideoWriter(?:.getProfiles)?|view|viewmtx|visdiff|volumebounds|voronoi|voronoin|wait|waitbar|waitfor|waitforbuttonpress|warndlg|warning|waterfall|wavfinfo|wavplay|wavread|wavrecord|wavwrite|web|weekday|what|whatsnew|which|whitebg|who|whos|wilkinson|winopen|winqueryreg|winter|wk1finfo|wk1read|wk1write|workspace|xlabel|xlim|xlsfinfo|xlsread|xlswrite|xmlread|xmlwrite|xor|xslt|ylabel|ylim|zeros|zip|zlabel|zlim|zoom)\b/, a],["fun_tbx",/^\b(?:addedvarplot|andrewsplot|anova[12n]|ansaribradley|aoctool|barttest|bbdesign|beta(?:cdf|fit|inv|like|pdf|rnd|stat)|bino(?:cdf|fit|inv|pdf|rnd|stat)|biplot|bootci|bootstrp|boxplot|candexch|candgen|canoncorr|capability|capaplot|caseread|casewrite|categorical|ccdesign|cdfplot|chi2(?:cdf|gof|inv|pdf|rnd|stat)|cholcov|Classification(?:BaggedEnsemble|Discriminant(?:.(?:fit|make|template))?|Ensemble|KNN(?:.(?:fit|template))?|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|classify|classregtree|cluster|clusterdata|cmdscale|combnk|Compact(?:Classification(?:Discriminant|Ensemble|Tree)|Regression(?:Ensemble|Tree)|TreeBagger)|confusionmat|controlchart|controlrules|cophenet|copula(?:cdf|fit|param|pdf|rnd|stat)|cordexch|corr|corrcov|coxphfit|createns|crosstab|crossval|cvpartition|datasample|dataset|daugment|dcovary|dendrogram|dfittool|disttool|dummyvar|dwtest|ecdf|ecdfhist|ev(?:cdf|fit|inv|like|pdf|rnd|stat)|ExhaustiveSearcher|exp(?:cdf|fit|inv|like|pdf|rnd|stat)|factoran|fcdf|ff2n|finv|fitdist|fitensemble|fpdf|fracfact|fracfactgen|friedman|frnd|fstat|fsurfht|fullfact|gagerr|gam(?:cdf|fit|inv|like|pdf|rnd|stat)|GeneralizedLinearModel(?:.fit)?|geo(?:cdf|inv|mean|pdf|rnd|stat)|gev(?:cdf|fit|inv|like|pdf|rnd|stat)|gline|glmfit|glmval|glyphplot|gmdistribution(?:.fit)?|gname|gp(?:cdf|fit|inv|like|pdf|rnd|stat)|gplotmatrix|grp2idx|grpstats|gscatter|haltonset|harmmean|hist3|histfit|hmm(?:decode|estimate|generate|train|viterbi)|hougen|hyge(?:cdf|inv|pdf|rnd|stat)|icdf|inconsistent|interactionplot|invpred|iqr|iwishrnd|jackknife|jbtest|johnsrnd|KDTreeSearcher|kmeans|knnsearch|kruskalwallis|ksdensity|kstest|kstest2|kurtosis|lasso|lassoglm|lassoPlot|leverage|lhsdesign|lhsnorm|lillietest|LinearModel(?:.fit)?|linhyptest|linkage|logn(?:cdf|fit|inv|like|pdf|rnd|stat)|lsline|mad|mahal|maineffectsplot|manova1|manovacluster|mdscale|mhsample|mle|mlecov|mnpdf|mnrfit|mnrnd|mnrval|moment|multcompare|multivarichart|mvn(?:cdf|pdf|rnd)|mvregress|mvregresslike|mvt(?:cdf|pdf|rnd)|NaiveBayes(?:.fit)?|nan(?:cov|max|mean|median|min|std|sum|var)|nbin(?:cdf|fit|inv|pdf|rnd|stat)|ncf(?:cdf|inv|pdf|rnd|stat)|nct(?:cdf|inv|pdf|rnd|stat)|ncx2(?:cdf|inv|pdf|rnd|stat)|NeighborSearcher|nlinfit|nlintool|nlmefit|nlmefitsa|nlparci|nlpredci|nnmf|nominal|NonLinearModel(?:.fit)?|norm(?:cdf|fit|inv|like|pdf|rnd|stat)|normplot|normspec|ordinal|outlierMeasure|parallelcoords|paretotails|partialcorr|pcacov|pcares|pdf|pdist|pdist2|pearsrnd|perfcurve|perms|piecewisedistribution|plsregress|poiss(?:cdf|fit|inv|pdf|rnd|tat)|polyconf|polytool|prctile|princomp|ProbDist(?:Kernel|Parametric|UnivKernel|UnivParam)?|probplot|procrustes|qqplot|qrandset|qrandstream|quantile|randg|random|randsample|randtool|range|rangesearch|ranksum|rayl(?:cdf|fit|inv|pdf|rnd|stat)|rcoplot|refcurve|refline|regress|Regression(?:BaggedEnsemble|Ensemble|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|regstats|relieff|ridge|robustdemo|robustfit|rotatefactors|rowexch|rsmdemo|rstool|runstest|sampsizepwr|scatterhist|sequentialfs|signrank|signtest|silhouette|skewness|slicesample|sobolset|squareform|statget|statset|stepwise|stepwisefit|surfht|tabulate|tblread|tblwrite|tcdf|tdfread|tiedrank|tinv|tpdf|TreeBagger|treedisp|treefit|treeprune|treetest|treeval|trimmean|trnd|tstat|ttest|ttest2|unid(?:cdf|inv|pdf|rnd|stat)|unif(?:cdf|inv|it|pdf|rnd|stat)|vartest(?:2|n)?|wbl(?:cdf|fit|inv|like|pdf|rnd|stat)|wblplot|wishrnd|x2fx|xptread|zscore|ztest)\b/, a],["fun_tbx",/^\b(?:adapthisteq|analyze75info|analyze75read|applycform|applylut|axes2pix|bestblk|blockproc|bwarea|bwareaopen|bwboundaries|bwconncomp|bwconvhull|bwdist|bwdistgeodesic|bweuler|bwhitmiss|bwlabel|bwlabeln|bwmorph|bwpack|bwperim|bwselect|bwtraceboundary|bwulterode|bwunpack|checkerboard|col2im|colfilt|conndef|convmtx2|corner|cornermetric|corr2|cp2tform|cpcorr|cpselect|cpstruct2pairs|dct2|dctmtx|deconvblind|deconvlucy|deconvreg|deconvwnr|decorrstretch|demosaic|dicom(?:anon|dict|info|lookup|read|uid|write)|edge|edgetaper|entropy|entropyfilt|fan2para|fanbeam|findbounds|fliptform|freqz2|fsamp2|fspecial|ftrans2|fwind1|fwind2|getheight|getimage|getimagemodel|getline|getneighbors|getnhood|getpts|getrangefromclass|getrect|getsequence|gray2ind|graycomatrix|graycoprops|graydist|grayslice|graythresh|hdrread|hdrwrite|histeq|hough|houghlines|houghpeaks|iccfind|iccread|iccroot|iccwrite|idct2|ifanbeam|im2bw|im2col|im2double|im2int16|im2java2d|im2single|im2uint16|im2uint8|imabsdiff|imadd|imadjust|ImageAdapter|imageinfo|imagemodel|imapplymatrix|imattributes|imbothat|imclearborder|imclose|imcolormaptool|imcomplement|imcontour|imcontrast|imcrop|imdilate|imdisplayrange|imdistline|imdivide|imellipse|imerode|imextendedmax|imextendedmin|imfill|imfilter|imfindcircles|imfreehand|imfuse|imgca|imgcf|imgetfile|imhandles|imhist|imhmax|imhmin|imimposemin|imlincomb|imline|immagbox|immovie|immultiply|imnoise|imopen|imoverview|imoverviewpanel|impixel|impixelinfo|impixelinfoval|impixelregion|impixelregionpanel|implay|impoint|impoly|impositionrect|improfile|imputfile|impyramid|imreconstruct|imrect|imregconfig|imregionalmax|imregionalmin|imregister|imresize|imroi|imrotate|imsave|imscrollpanel|imshow|imshowpair|imsubtract|imtool|imtophat|imtransform|imview|ind2gray|ind2rgb|interfileinfo|interfileread|intlut|ippl|iptaddcallback|iptcheckconn|iptcheckhandle|iptcheckinput|iptcheckmap|iptchecknargin|iptcheckstrs|iptdemos|iptgetapi|iptGetPointerBehavior|iptgetpref|ipticondir|iptnum2ordinal|iptPointerManager|iptprefs|iptremovecallback|iptSetPointerBehavior|iptsetpref|iptwindowalign|iradon|isbw|isflat|isgray|isicc|isind|isnitf|isrgb|isrset|lab2double|lab2uint16|lab2uint8|label2rgb|labelmatrix|makecform|makeConstrainToRectFcn|makehdr|makelut|makeresampler|maketform|mat2gray|mean2|medfilt2|montage|nitfinfo|nitfread|nlfilter|normxcorr2|ntsc2rgb|openrset|ordfilt2|otf2psf|padarray|para2fan|phantom|poly2mask|psf2otf|qtdecomp|qtgetblk|qtsetblk|radon|rangefilt|reflect|regionprops|registration.metric.(?:MattesMutualInformation|MeanSquares)|registration.optimizer.(?:OnePlusOneEvolutionary|RegularStepGradientDescent)|rgb2gray|rgb2ntsc|rgb2ycbcr|roicolor|roifill|roifilt2|roipoly|rsetwrite|std2|stdfilt|strel|stretchlim|subimage|tformarray|tformfwd|tforminv|tonemap|translate|truesize|uintlut|viscircles|warp|watershed|whitepoint|wiener2|xyz2double|xyz2uint16|ycbcr2rgb)\b/, a],["fun_tbx",/^\b(?:bintprog|color|fgoalattain|fminbnd|fmincon|fminimax|fminsearch|fminunc|fseminf|fsolve|fzero|fzmult|gangstr|ktrlink|linprog|lsqcurvefit|lsqlin|lsqnonlin|lsqnonneg|optimget|optimset|optimtool|quadprog)\b/,a],["ident",/^[A-Za-z]\w*(?:\.[A-Za-z]\w*)*/,a]]),["matlab-identifiers"]);b.registerLangHandler(b.createSimpleLexer([],e),["matlab-operators"]);b.registerLangHandler(b.createSimpleLexer(c,d),["matlab"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-ml.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^#(?:if[\t\n\r \xa0]+(?:[$_a-z][\w']*|``[^\t\n\r`]*(?:``|$))|else|endif|light)/i,null,"#"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])(?:'|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\(\*[\S\s]*?\*\))/],["kwd",/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/], ["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^(?:[_a-z][\w']*[!#?]?|``[^\t\n\r`]*(?:``|$))/i],["pun",/^[^\w\t\n\r "'\xa0]+/]]),["fs","ml"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-mumps.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"]|\\.)*"/,null,'"']],[["com",/^;[^\n\r]*/,null,";"],["dec",/^\$(?:d|device|ec|ecode|es|estack|et|etrap|h|horolog|i|io|j|job|k|key|p|principal|q|quit|st|stack|s|storage|sy|system|t|test|tl|tlevel|tr|trestart|x|y|z[a-z]*|a|ascii|c|char|d|data|e|extract|f|find|fn|fnumber|g|get|j|justify|l|length|na|name|o|order|p|piece|ql|qlength|qs|qsubscript|q|query|r|random|re|reverse|s|select|st|stack|t|text|tr|translate|nan)\b/i, null],["kwd",/^(?:[^$]b|break|c|close|d|do|e|else|f|for|g|goto|h|halt|h|hang|i|if|j|job|k|kill|l|lock|m|merge|n|new|o|open|q|quit|r|read|s|set|tc|tcommit|tre|trestart|tro|trollback|ts|tstart|u|use|v|view|w|write|x|xecute)\b/i,null],["lit",/^[+-]?(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?/i],["pln",/^[a-z][^\W_]*/i],["pun",/^[^\w\t\n\r"$%;^\xa0]|_/]]),["mumps"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-n.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["str",/^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,a,'"'],["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,a,"#"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["str",/^@"(?:[^"]|"")*(?:"|$)/,a],["str",/^<#[^#>]*(?:#>|$)/,a],["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,a],["com",/^\/\/[^\n\r]*/,a],["com",/^\/\*[\S\s]*?(?:\*\/|$)/, a],["kwd",/^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/, a],["typ",/^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/,a],["lit",/^@[$_a-z][\w$@]*/i,a],["typ",/^@[A-Z]+[a-z][\w$@]*/,a],["pln",/^'?[$_a-z][\w$@]*/i,a],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,a,"0123456789"],["pun",/^.[^\s\w"-$'./@`]*/,a]]),["n","nemerle"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-pascal.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["str",/^'(?:[^\n\r'\\]|\\.)*(?:'|$)/,a,"'"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^\(\*[\S\s]*?(?:\*\)|$)|^{[\S\s]*?(?:}|$)/,a],["kwd",/^(?:absolute|and|array|asm|assembler|begin|case|const|constructor|destructor|div|do|downto|else|end|external|for|forward|function|goto|if|implementation|in|inline|interface|interrupt|label|mod|not|object|of|or|packed|procedure|program|record|repeat|set|shl|shr|then|to|type|unit|until|uses|var|virtual|while|with|xor)\b/i,a], ["lit",/^(?:true|false|self|nil)/i,a],["pln",/^[a-z][^\W_]*/i,a],["lit",/^(?:\$[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/i,a,"0123456789"],["pun",/^.[^\s\w$'./@]*/,a]]),["pascal"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-proto.js ================================================ PR.registerLangHandler(PR.sourceDecorator({keywords:"bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true",types:/^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/,cStyleComments:!0}),["proto"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-r.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^'\\]|\\[\S\s])*(?:'|$)/,null,"'"]],[["com",/^#.*/],["kwd",/^(?:if|else|for|while|repeat|in|next|break|return|switch|function)(?![\w.])/],["lit",/^0[Xx][\dA-Fa-f]+([Pp]\d+)?[Li]?/],["lit",/^[+-]?(\d+(\.\d+)?|\.\d+)([Ee][+-]?\d+)?[Li]?/],["lit",/^(?:NULL|NA(?:_(?:integer|real|complex|character)_)?|Inf|TRUE|FALSE|NaN|\.\.(?:\.|\d+))(?![\w.])/], ["pun",/^(?:<>?|-|==|<=|>=|<|>|&&?|!=|\|\|?|[!*+/^]|%.*?%|[$=@~]|:{1,3}|[(),;?[\]{}])/],["pln",/^(?:[A-Za-z]+[\w.]*|\.[^\W\d][\w.]*)(?![\w.])/],["str",/^`.+`/]]),["r","s","R","S","Splus"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-rd.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["lit",/^\\(?:cr|l?dots|R|tab)\b/],["kwd",/^\\[@-Za-z]+/],["kwd",/^#(?:ifn?def|endif)/],["pln",/^\\[{}]/],["pun",/^[()[\]{}]+/]]),["Rd","rd"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-scala.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:""(?:""?(?!")|[^"\\]|\\.)*"{0,3}|(?:[^\n\r"\\]|\\.)*"?)/,null,'"'],["lit",/^`(?:[^\n\r\\`]|\\.)*`?/,null,"`"],["pun",/^[!#%&(--:-@[-^{-~]+/,null,"!#%&()*+,-:;<=>?@[\\]^{|}~"]],[["str",/^'(?:[^\n\r'\\]|\\(?:'|[^\n\r']+))'/],["lit",/^'[$A-Z_a-z][\w$]*(?![\w$'])/],["kwd",/^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/], ["lit",/^(?:true|false|null|this)\b/],["lit",/^(?:0(?:[0-7]+|x[\da-f]+)l?|(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:e[+-]?\d+)?f?|l?)|\\.\d+(?:e[+-]?\d+)?f?)/i],["typ",/^[$_]*[A-Z][\d$A-Z_]*[a-z][\w$]*/],["pln",/^[$A-Z_a-z][\w$]*/],["com",/^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/],["pun",/^(?:\.+|\/)/]]),["scala"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-sql.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|connect|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|matched|merge|natural|national|nocheck|nonclustered|nocycle|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|partition|percent|pivot|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|start|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|unpivot|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|within|writetext|xml)(?=[^\w-]|$)/i, null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-tcl.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["opn",/^{+/,a,"{"],["clo",/^}+/,a,"}"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:after|append|apply|array|break|case|catch|continue|error|eval|exec|exit|expr|for|foreach|if|incr|info|proc|return|set|switch|trace|uplevel|upvar|while)\b/,a],["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit", /^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["tcl"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-tex.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["kwd",/^\\[@-Za-z]+/],["kwd",/^\\./],["typ",/^[$&]/],["lit",/[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i],["pun",/^[()=[\]{}]+/]]),["latex","tex"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-vb.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0\u2028\u2029]+/,null,"\t\n\r \u00a0\u2028\u2029"],["str",/^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i,null,'"\u201c\u201d'],["com",/^['\u2018\u2019](?:_(?:\r\n?|[^\r]?)|[^\n\r_\u2028\u2029])*/,null,"'\u2018\u2019"]],[["kwd",/^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i, null],["com",/^rem\b.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*(?:\[[!#%&@]+])?|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-vhdl.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["str",/^(?:[box]?"(?:[^"]|"")*"|'.')/i],["com",/^--[^\n\r]*/],["kwd",/^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i, null],["typ",/^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i,null],["typ",/^'(?:active|ascending|base|delayed|driving|driving_value|event|high|image|instance_name|last_active|last_event|last_value|left|leftof|length|low|path_name|pos|pred|quiet|range|reverse_range|right|rightof|simple_name|stable|succ|transaction|val|value)(?=[^\w-]|$)/i,null],["lit",/^\d+(?:_\d+)*(?:#[\w.\\]+#(?:[+-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:e[+-]?\d+(?:_\d+)*)?)/i], ["pln",/^(?:[a-z]\w*|\\[^\\]*\\)/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'\xa0-]*/]]),["vhdl","vhd"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-wiki.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\d\t a-gi-z\xa0]+/,null,"\t \u00a0abcdefgijklmnopqrstuvwxyz0123456789"],["pun",/^[*=[\]^~]+/,null,"=*~^[]"]],[["lang-wiki.meta",/(?:^^|\r\n?|\n)(#[a-z]+)\b/],["lit",/^[A-Z][a-z][\da-z]+[A-Z][a-z][^\W_]+\b/],["lang-",/^{{{([\S\s]+?)}}}/],["lang-",/^`([^\n\r`]+)`/],["str",/^https?:\/\/[^\s#/?]*(?:\/[^\s#?]*)?(?:\?[^\s#]*)?(?:#\S*)?/i],["pln",/^(?:\r\n|[\S\s])[^\n\r#*=A-[^`h{~]*/]]),["wiki"]); PR.registerLangHandler(PR.createSimpleLexer([["kwd",/^#[a-z]+/i,null,"#"]],[]),["wiki.meta"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-xq.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["var pln",/^\$[\w-]+/,null,"$"]],[["pln",/^[\s=][<>][\s=]/],["lit",/^@[\w-]+/],["tag",/^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["com",/^\(:[\S\s]*?:\)/],["pln",/^[(),/;[\]{}]$/],["str",/^(?:"(?:[^"\\{]|\\[\S\s])*(?:"|$)|'(?:[^'\\{]|\\[\S\s])*(?:'|$))/,null,"\"'"],["kwd",/^(?:xquery|where|version|variable|union|typeswitch|treat|to|then|text|stable|sortby|some|self|schema|satisfies|returns|return|ref|processing-instruction|preceding-sibling|preceding|precedes|parent|only|of|node|namespace|module|let|item|intersect|instance|in|import|if|function|for|follows|following-sibling|following|external|except|every|else|element|descending|descendant-or-self|descendant|define|default|declare|comment|child|cast|case|before|attribute|assert|ascending|as|ancestor-or-self|ancestor|after|eq|order|by|or|and|schema-element|document-node|node|at)\b/], ["typ",/^(?:xs:yearMonthDuration|xs:unsignedLong|xs:time|xs:string|xs:short|xs:QName|xs:Name|xs:long|xs:integer|xs:int|xs:gYearMonth|xs:gYear|xs:gMonthDay|xs:gDay|xs:float|xs:duration|xs:double|xs:decimal|xs:dayTimeDuration|xs:dateTime|xs:date|xs:byte|xs:boolean|xs:anyURI|xf:yearMonthDuration)\b/,null],["fun pln",/^(?:xp:dereference|xinc:node-expand|xinc:link-references|xinc:link-expand|xhtml:restructure|xhtml:clean|xhtml:add-lists|xdmp:zip-manifest|xdmp:zip-get|xdmp:zip-create|xdmp:xquery-version|xdmp:word-convert|xdmp:with-namespaces|xdmp:version|xdmp:value|xdmp:user-roles|xdmp:user-last-login|xdmp:user|xdmp:url-encode|xdmp:url-decode|xdmp:uri-is-file|xdmp:uri-format|xdmp:uri-content-type|xdmp:unquote|xdmp:unpath|xdmp:triggers-database|xdmp:trace|xdmp:to-json|xdmp:tidy|xdmp:subbinary|xdmp:strftime|xdmp:spawn-in|xdmp:spawn|xdmp:sleep|xdmp:shutdown|xdmp:set-session-field|xdmp:set-response-encoding|xdmp:set-response-content-type|xdmp:set-response-code|xdmp:set-request-time-limit|xdmp:set|xdmp:servers|xdmp:server-status|xdmp:server-name|xdmp:server|xdmp:security-database|xdmp:security-assert|xdmp:schema-database|xdmp:save|xdmp:role-roles|xdmp:role|xdmp:rethrow|xdmp:restart|xdmp:request-timestamp|xdmp:request-status|xdmp:request-cancel|xdmp:request|xdmp:redirect-response|xdmp:random|xdmp:quote|xdmp:query-trace|xdmp:query-meters|xdmp:product-edition|xdmp:privilege-roles|xdmp:privilege|xdmp:pretty-print|xdmp:powerpoint-convert|xdmp:platform|xdmp:permission|xdmp:pdf-convert|xdmp:path|xdmp:octal-to-integer|xdmp:node-uri|xdmp:node-replace|xdmp:node-kind|xdmp:node-insert-child|xdmp:node-insert-before|xdmp:node-insert-after|xdmp:node-delete|xdmp:node-database|xdmp:mul64|xdmp:modules-root|xdmp:modules-database|xdmp:merging|xdmp:merge-cancel|xdmp:merge|xdmp:md5|xdmp:logout|xdmp:login|xdmp:log-level|xdmp:log|xdmp:lock-release|xdmp:lock-acquire|xdmp:load|xdmp:invoke-in|xdmp:invoke|xdmp:integer-to-octal|xdmp:integer-to-hex|xdmp:http-put|xdmp:http-post|xdmp:http-options|xdmp:http-head|xdmp:http-get|xdmp:http-delete|xdmp:hosts|xdmp:host-status|xdmp:host-name|xdmp:host|xdmp:hex-to-integer|xdmp:hash64|xdmp:hash32|xdmp:has-privilege|xdmp:groups|xdmp:group-serves|xdmp:group-servers|xdmp:group-name|xdmp:group-hosts|xdmp:group|xdmp:get-session-field-names|xdmp:get-session-field|xdmp:get-response-encoding|xdmp:get-response-code|xdmp:get-request-username|xdmp:get-request-user|xdmp:get-request-url|xdmp:get-request-protocol|xdmp:get-request-path|xdmp:get-request-method|xdmp:get-request-header-names|xdmp:get-request-header|xdmp:get-request-field-names|xdmp:get-request-field-filename|xdmp:get-request-field-content-type|xdmp:get-request-field|xdmp:get-request-client-certificate|xdmp:get-request-client-address|xdmp:get-request-body|xdmp:get-current-user|xdmp:get-current-roles|xdmp:get|xdmp:function-name|xdmp:function-module|xdmp:function|xdmp:from-json|xdmp:forests|xdmp:forest-status|xdmp:forest-restore|xdmp:forest-restart|xdmp:forest-name|xdmp:forest-delete|xdmp:forest-databases|xdmp:forest-counts|xdmp:forest-clear|xdmp:forest-backup|xdmp:forest|xdmp:filesystem-file|xdmp:filesystem-directory|xdmp:exists|xdmp:excel-convert|xdmp:eval-in|xdmp:eval|xdmp:estimate|xdmp:email|xdmp:element-content-type|xdmp:elapsed-time|xdmp:document-set-quality|xdmp:document-set-property|xdmp:document-set-properties|xdmp:document-set-permissions|xdmp:document-set-collections|xdmp:document-remove-properties|xdmp:document-remove-permissions|xdmp:document-remove-collections|xdmp:document-properties|xdmp:document-locks|xdmp:document-load|xdmp:document-insert|xdmp:document-get-quality|xdmp:document-get-properties|xdmp:document-get-permissions|xdmp:document-get-collections|xdmp:document-get|xdmp:document-forest|xdmp:document-delete|xdmp:document-add-properties|xdmp:document-add-permissions|xdmp:document-add-collections|xdmp:directory-properties|xdmp:directory-locks|xdmp:directory-delete|xdmp:directory-create|xdmp:directory|xdmp:diacritic-less|xdmp:describe|xdmp:default-permissions|xdmp:default-collections|xdmp:databases|xdmp:database-restore-validate|xdmp:database-restore-status|xdmp:database-restore-cancel|xdmp:database-restore|xdmp:database-name|xdmp:database-forests|xdmp:database-backup-validate|xdmp:database-backup-status|xdmp:database-backup-purge|xdmp:database-backup-cancel|xdmp:database-backup|xdmp:database|xdmp:collection-properties|xdmp:collection-locks|xdmp:collection-delete|xdmp:collation-canonical-uri|xdmp:castable-as|xdmp:can-grant-roles|xdmp:base64-encode|xdmp:base64-decode|xdmp:architecture|xdmp:apply|xdmp:amp-roles|xdmp:amp|xdmp:add64|xdmp:add-response-header|xdmp:access|trgr:trigger-set-recursive|trgr:trigger-set-permissions|trgr:trigger-set-name|trgr:trigger-set-module|trgr:trigger-set-event|trgr:trigger-set-description|trgr:trigger-remove-permissions|trgr:trigger-module|trgr:trigger-get-permissions|trgr:trigger-enable|trgr:trigger-disable|trgr:trigger-database-online-event|trgr:trigger-data-event|trgr:trigger-add-permissions|trgr:remove-trigger|trgr:property-content|trgr:pre-commit|trgr:post-commit|trgr:get-trigger-by-id|trgr:get-trigger|trgr:document-scope|trgr:document-content|trgr:directory-scope|trgr:create-trigger|trgr:collection-scope|trgr:any-property-content|thsr:set-entry|thsr:remove-term|thsr:remove-synonym|thsr:remove-entry|thsr:query-lookup|thsr:lookup|thsr:load|thsr:insert|thsr:expand|thsr:add-synonym|spell:suggest-detailed|spell:suggest|spell:remove-word|spell:make-dictionary|spell:load|spell:levenshtein-distance|spell:is-correct|spell:insert|spell:double-metaphone|spell:add-word|sec:users-collection|sec:user-set-roles|sec:user-set-password|sec:user-set-name|sec:user-set-description|sec:user-set-default-permissions|sec:user-set-default-collections|sec:user-remove-roles|sec:user-privileges|sec:user-get-roles|sec:user-get-description|sec:user-get-default-permissions|sec:user-get-default-collections|sec:user-doc-permissions|sec:user-doc-collections|sec:user-add-roles|sec:unprotect-collection|sec:uid-for-name|sec:set-realm|sec:security-version|sec:security-namespace|sec:security-installed|sec:security-collection|sec:roles-collection|sec:role-set-roles|sec:role-set-name|sec:role-set-description|sec:role-set-default-permissions|sec:role-set-default-collections|sec:role-remove-roles|sec:role-privileges|sec:role-get-roles|sec:role-get-description|sec:role-get-default-permissions|sec:role-get-default-collections|sec:role-doc-permissions|sec:role-doc-collections|sec:role-add-roles|sec:remove-user|sec:remove-role-from-users|sec:remove-role-from-role|sec:remove-role-from-privileges|sec:remove-role-from-amps|sec:remove-role|sec:remove-privilege|sec:remove-amp|sec:protect-collection|sec:privileges-collection|sec:privilege-set-roles|sec:privilege-set-name|sec:privilege-remove-roles|sec:privilege-get-roles|sec:privilege-add-roles|sec:priv-doc-permissions|sec:priv-doc-collections|sec:get-user-names|sec:get-unique-elem-id|sec:get-role-names|sec:get-role-ids|sec:get-privilege|sec:get-distinct-permissions|sec:get-collection|sec:get-amp|sec:create-user-with-role|sec:create-user|sec:create-role|sec:create-privilege|sec:create-amp|sec:collections-collection|sec:collection-set-permissions|sec:collection-remove-permissions|sec:collection-get-permissions|sec:collection-add-permissions|sec:check-admin|sec:amps-collection|sec:amp-set-roles|sec:amp-remove-roles|sec:amp-get-roles|sec:amp-doc-permissions|sec:amp-doc-collections|sec:amp-add-roles|search:unparse|search:suggest|search:snippet|search:search|search:resolve-nodes|search:resolve|search:remove-constraint|search:parse|search:get-default-options|search:estimate|search:check-options|prof:value|prof:reset|prof:report|prof:invoke|prof:eval|prof:enable|prof:disable|prof:allowed|ppt:clean|pki:template-set-request|pki:template-set-name|pki:template-set-key-type|pki:template-set-key-options|pki:template-set-description|pki:template-in-use|pki:template-get-version|pki:template-get-request|pki:template-get-name|pki:template-get-key-type|pki:template-get-key-options|pki:template-get-id|pki:template-get-description|pki:need-certificate|pki:is-temporary|pki:insert-trusted-certificates|pki:insert-template|pki:insert-signed-certificates|pki:insert-certificate-revocation-list|pki:get-trusted-certificate-ids|pki:get-template-ids|pki:get-template-certificate-authority|pki:get-template-by-name|pki:get-template|pki:get-pending-certificate-requests-xml|pki:get-pending-certificate-requests-pem|pki:get-pending-certificate-request|pki:get-certificates-for-template-xml|pki:get-certificates-for-template|pki:get-certificates|pki:get-certificate-xml|pki:get-certificate-pem|pki:get-certificate|pki:generate-temporary-certificate-if-necessary|pki:generate-temporary-certificate|pki:generate-template-certificate-authority|pki:generate-certificate-request|pki:delete-template|pki:delete-certificate|pki:create-template|pdf:make-toc|pdf:insert-toc-headers|pdf:get-toc|pdf:clean|p:status-transition|p:state-transition|p:remove|p:pipelines|p:insert|p:get-by-id|p:get|p:execute|p:create|p:condition|p:collection|p:action|ooxml:runs-merge|ooxml:package-uris|ooxml:package-parts-insert|ooxml:package-parts|msword:clean|mcgm:polygon|mcgm:point|mcgm:geospatial-query-from-elements|mcgm:geospatial-query|mcgm:circle|math:tanh|math:tan|math:sqrt|math:sinh|math:sin|math:pow|math:modf|math:log10|math:log|math:ldexp|math:frexp|math:fmod|math:floor|math:fabs|math:exp|math:cosh|math:cos|math:ceil|math:atan2|math:atan|math:asin|math:acos|map:put|map:map|map:keys|map:get|map:delete|map:count|map:clear|lnk:to|lnk:remove|lnk:insert|lnk:get|lnk:from|lnk:create|kml:polygon|kml:point|kml:interior-polygon|kml:geospatial-query-from-elements|kml:geospatial-query|kml:circle|kml:box|gml:polygon|gml:point|gml:interior-polygon|gml:geospatial-query-from-elements|gml:geospatial-query|gml:circle|gml:box|georss:point|georss:geospatial-query|georss:circle|geo:polygon|geo:point|geo:interior-polygon|geo:geospatial-query-from-elements|geo:geospatial-query|geo:circle|geo:box|fn:zero-or-one|fn:years-from-duration|fn:year-from-dateTime|fn:year-from-date|fn:upper-case|fn:unordered|fn:true|fn:translate|fn:trace|fn:tokenize|fn:timezone-from-time|fn:timezone-from-dateTime|fn:timezone-from-date|fn:sum|fn:subtract-dateTimes-yielding-yearMonthDuration|fn:subtract-dateTimes-yielding-dayTimeDuration|fn:substring-before|fn:substring-after|fn:substring|fn:subsequence|fn:string-to-codepoints|fn:string-pad|fn:string-length|fn:string-join|fn:string|fn:static-base-uri|fn:starts-with|fn:seconds-from-time|fn:seconds-from-duration|fn:seconds-from-dateTime|fn:round-half-to-even|fn:round|fn:root|fn:reverse|fn:resolve-uri|fn:resolve-QName|fn:replace|fn:remove|fn:QName|fn:prefix-from-QName|fn:position|fn:one-or-more|fn:number|fn:not|fn:normalize-unicode|fn:normalize-space|fn:node-name|fn:node-kind|fn:nilled|fn:namespace-uri-from-QName|fn:namespace-uri-for-prefix|fn:namespace-uri|fn:name|fn:months-from-duration|fn:month-from-dateTime|fn:month-from-date|fn:minutes-from-time|fn:minutes-from-duration|fn:minutes-from-dateTime|fn:min|fn:max|fn:matches|fn:lower-case|fn:local-name-from-QName|fn:local-name|fn:last|fn:lang|fn:iri-to-uri|fn:insert-before|fn:index-of|fn:in-scope-prefixes|fn:implicit-timezone|fn:idref|fn:id|fn:hours-from-time|fn:hours-from-duration|fn:hours-from-dateTime|fn:floor|fn:false|fn:expanded-QName|fn:exists|fn:exactly-one|fn:escape-uri|fn:escape-html-uri|fn:error|fn:ends-with|fn:encode-for-uri|fn:empty|fn:document-uri|fn:doc-available|fn:doc|fn:distinct-values|fn:distinct-nodes|fn:default-collation|fn:deep-equal|fn:days-from-duration|fn:day-from-dateTime|fn:day-from-date|fn:data|fn:current-time|fn:current-dateTime|fn:current-date|fn:count|fn:contains|fn:concat|fn:compare|fn:collection|fn:codepoints-to-string|fn:codepoint-equal|fn:ceiling|fn:boolean|fn:base-uri|fn:avg|fn:adjust-time-to-timezone|fn:adjust-dateTime-to-timezone|fn:adjust-date-to-timezone|fn:abs|feed:unsubscribe|feed:subscription|feed:subscribe|feed:request|feed:item|feed:description|excel:clean|entity:enrich|dom:set-pipelines|dom:set-permissions|dom:set-name|dom:set-evaluation-context|dom:set-domain-scope|dom:set-description|dom:remove-pipeline|dom:remove-permissions|dom:remove|dom:get|dom:evaluation-context|dom:domains|dom:domain-scope|dom:create|dom:configuration-set-restart-user|dom:configuration-set-permissions|dom:configuration-set-evaluation-context|dom:configuration-set-default-domain|dom:configuration-get|dom:configuration-create|dom:collection|dom:add-pipeline|dom:add-permissions|dls:retention-rules|dls:retention-rule-remove|dls:retention-rule-insert|dls:retention-rule|dls:purge|dls:node-expand|dls:link-references|dls:link-expand|dls:documents-query|dls:document-versions-query|dls:document-version-uri|dls:document-version-query|dls:document-version-delete|dls:document-version-as-of|dls:document-version|dls:document-update|dls:document-unmanage|dls:document-set-quality|dls:document-set-property|dls:document-set-properties|dls:document-set-permissions|dls:document-set-collections|dls:document-retention-rules|dls:document-remove-properties|dls:document-remove-permissions|dls:document-remove-collections|dls:document-purge|dls:document-manage|dls:document-is-managed|dls:document-insert-and-manage|dls:document-include-query|dls:document-history|dls:document-get-permissions|dls:document-extract-part|dls:document-delete|dls:document-checkout-status|dls:document-checkout|dls:document-checkin|dls:document-add-properties|dls:document-add-permissions|dls:document-add-collections|dls:break-checkout|dls:author-query|dls:as-of-query|dbk:convert|dbg:wait|dbg:value|dbg:stopped|dbg:stop|dbg:step|dbg:status|dbg:stack|dbg:out|dbg:next|dbg:line|dbg:invoke|dbg:function|dbg:finish|dbg:expr|dbg:eval|dbg:disconnect|dbg:detach|dbg:continue|dbg:connect|dbg:clear|dbg:breakpoints|dbg:break|dbg:attached|dbg:attach|cvt:save-converted-documents|cvt:part-uri|cvt:destination-uri|cvt:basepath|cvt:basename|cts:words|cts:word-query-weight|cts:word-query-text|cts:word-query-options|cts:word-query|cts:word-match|cts:walk|cts:uris|cts:uri-match|cts:train|cts:tokenize|cts:thresholds|cts:stem|cts:similar-query-weight|cts:similar-query-nodes|cts:similar-query|cts:shortest-distance|cts:search|cts:score|cts:reverse-query-weight|cts:reverse-query-nodes|cts:reverse-query|cts:remainder|cts:registered-query-weight|cts:registered-query-options|cts:registered-query-ids|cts:registered-query|cts:register|cts:query|cts:quality|cts:properties-query-query|cts:properties-query|cts:polygon-vertices|cts:polygon|cts:point-longitude|cts:point-latitude|cts:point|cts:or-query-queries|cts:or-query|cts:not-query-weight|cts:not-query-query|cts:not-query|cts:near-query-weight|cts:near-query-queries|cts:near-query-options|cts:near-query-distance|cts:near-query|cts:highlight|cts:geospatial-co-occurrences|cts:frequency|cts:fitness|cts:field-words|cts:field-word-query-weight|cts:field-word-query-text|cts:field-word-query-options|cts:field-word-query-field-name|cts:field-word-query|cts:field-word-match|cts:entity-highlight|cts:element-words|cts:element-word-query-weight|cts:element-word-query-text|cts:element-word-query-options|cts:element-word-query-element-name|cts:element-word-query|cts:element-word-match|cts:element-values|cts:element-value-ranges|cts:element-value-query-weight|cts:element-value-query-text|cts:element-value-query-options|cts:element-value-query-element-name|cts:element-value-query|cts:element-value-match|cts:element-value-geospatial-co-occurrences|cts:element-value-co-occurrences|cts:element-range-query-weight|cts:element-range-query-value|cts:element-range-query-options|cts:element-range-query-operator|cts:element-range-query-element-name|cts:element-range-query|cts:element-query-query|cts:element-query-element-name|cts:element-query|cts:element-pair-geospatial-values|cts:element-pair-geospatial-value-match|cts:element-pair-geospatial-query-weight|cts:element-pair-geospatial-query-region|cts:element-pair-geospatial-query-options|cts:element-pair-geospatial-query-longitude-name|cts:element-pair-geospatial-query-latitude-name|cts:element-pair-geospatial-query-element-name|cts:element-pair-geospatial-query|cts:element-pair-geospatial-boxes|cts:element-geospatial-values|cts:element-geospatial-value-match|cts:element-geospatial-query-weight|cts:element-geospatial-query-region|cts:element-geospatial-query-options|cts:element-geospatial-query-element-name|cts:element-geospatial-query|cts:element-geospatial-boxes|cts:element-child-geospatial-values|cts:element-child-geospatial-value-match|cts:element-child-geospatial-query-weight|cts:element-child-geospatial-query-region|cts:element-child-geospatial-query-options|cts:element-child-geospatial-query-element-name|cts:element-child-geospatial-query-child-name|cts:element-child-geospatial-query|cts:element-child-geospatial-boxes|cts:element-attribute-words|cts:element-attribute-word-query-weight|cts:element-attribute-word-query-text|cts:element-attribute-word-query-options|cts:element-attribute-word-query-element-name|cts:element-attribute-word-query-attribute-name|cts:element-attribute-word-query|cts:element-attribute-word-match|cts:element-attribute-values|cts:element-attribute-value-ranges|cts:element-attribute-value-query-weight|cts:element-attribute-value-query-text|cts:element-attribute-value-query-options|cts:element-attribute-value-query-element-name|cts:element-attribute-value-query-attribute-name|cts:element-attribute-value-query|cts:element-attribute-value-match|cts:element-attribute-value-geospatial-co-occurrences|cts:element-attribute-value-co-occurrences|cts:element-attribute-range-query-weight|cts:element-attribute-range-query-value|cts:element-attribute-range-query-options|cts:element-attribute-range-query-operator|cts:element-attribute-range-query-element-name|cts:element-attribute-range-query-attribute-name|cts:element-attribute-range-query|cts:element-attribute-pair-geospatial-values|cts:element-attribute-pair-geospatial-value-match|cts:element-attribute-pair-geospatial-query-weight|cts:element-attribute-pair-geospatial-query-region|cts:element-attribute-pair-geospatial-query-options|cts:element-attribute-pair-geospatial-query-longitude-name|cts:element-attribute-pair-geospatial-query-latitude-name|cts:element-attribute-pair-geospatial-query-element-name|cts:element-attribute-pair-geospatial-query|cts:element-attribute-pair-geospatial-boxes|cts:document-query-uris|cts:document-query|cts:distance|cts:directory-query-uris|cts:directory-query-depth|cts:directory-query|cts:destination|cts:deregister|cts:contains|cts:confidence|cts:collections|cts:collection-query-uris|cts:collection-query|cts:collection-match|cts:classify|cts:circle-radius|cts:circle-center|cts:circle|cts:box-west|cts:box-south|cts:box-north|cts:box-east|cts:box|cts:bearing|cts:arc-intersection|cts:and-query-queries|cts:and-query-options|cts:and-query|cts:and-not-query-positive-query|cts:and-not-query-negative-query|cts:and-not-query|css:get|css:convert|cpf:success|cpf:failure|cpf:document-set-state|cpf:document-set-processing-status|cpf:document-set-last-updated|cpf:document-set-error|cpf:document-get-state|cpf:document-get-processing-status|cpf:document-get-last-updated|cpf:document-get-error|cpf:check-transition|alert:spawn-matching-actions|alert:rule-user-id-query|alert:rule-set-user-id|alert:rule-set-query|alert:rule-set-options|alert:rule-set-name|alert:rule-set-description|alert:rule-set-action|alert:rule-remove|alert:rule-name-query|alert:rule-insert|alert:rule-id-query|alert:rule-get-user-id|alert:rule-get-query|alert:rule-get-options|alert:rule-get-name|alert:rule-get-id|alert:rule-get-description|alert:rule-get-action|alert:rule-action-query|alert:remove-triggers|alert:make-rule|alert:make-log-action|alert:make-config|alert:make-action|alert:invoke-matching-actions|alert:get-my-rules|alert:get-all-rules|alert:get-actions|alert:find-matching-rules|alert:create-triggers|alert:config-set-uri|alert:config-set-trigger-ids|alert:config-set-options|alert:config-set-name|alert:config-set-description|alert:config-set-cpf-domain-names|alert:config-set-cpf-domain-ids|alert:config-insert|alert:config-get-uri|alert:config-get-trigger-ids|alert:config-get-options|alert:config-get-name|alert:config-get-id|alert:config-get-description|alert:config-get-cpf-domain-names|alert:config-get-cpf-domain-ids|alert:config-get|alert:config-delete|alert:action-set-options|alert:action-set-name|alert:action-set-module-root|alert:action-set-module-db|alert:action-set-module|alert:action-set-description|alert:action-remove|alert:action-insert|alert:action-get-options|alert:action-get-name|alert:action-get-module-root|alert:action-get-module-db|alert:action-get-module|alert:action-get-description|zero-or-one|years-from-duration|year-from-dateTime|year-from-date|upper-case|unordered|true|translate|trace|tokenize|timezone-from-time|timezone-from-dateTime|timezone-from-date|sum|subtract-dateTimes-yielding-yearMonthDuration|subtract-dateTimes-yielding-dayTimeDuration|substring-before|substring-after|substring|subsequence|string-to-codepoints|string-pad|string-length|string-join|string|static-base-uri|starts-with|seconds-from-time|seconds-from-duration|seconds-from-dateTime|round-half-to-even|round|root|reverse|resolve-uri|resolve-QName|replace|remove|QName|prefix-from-QName|position|one-or-more|number|not|normalize-unicode|normalize-space|node-name|node-kind|nilled|namespace-uri-from-QName|namespace-uri-for-prefix|namespace-uri|name|months-from-duration|month-from-dateTime|month-from-date|minutes-from-time|minutes-from-duration|minutes-from-dateTime|min|max|matches|lower-case|local-name-from-QName|local-name|last|lang|iri-to-uri|insert-before|index-of|in-scope-prefixes|implicit-timezone|idref|id|hours-from-time|hours-from-duration|hours-from-dateTime|floor|false|expanded-QName|exists|exactly-one|escape-uri|escape-html-uri|error|ends-with|encode-for-uri|empty|document-uri|doc-available|doc|distinct-values|distinct-nodes|default-collation|deep-equal|days-from-duration|day-from-dateTime|day-from-date|data|current-time|current-dateTime|current-date|count|contains|concat|compare|collection|codepoints-to-string|codepoint-equal|ceiling|boolean|base-uri|avg|adjust-time-to-timezone|adjust-dateTime-to-timezone|adjust-date-to-timezone|abs)\b/], ["pln",/^[\w:-]+/],["pln",/^[\t\n\r \xa0]+/]]),["xq","xquery"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/lang-yaml.js ================================================ var a=null; PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:>?|]+/,a,":|>?"],["dec",/^%(?:YAML|TAG)[^\n\r#]+/,a,"%"],["typ",/^&\S+/,a,"&"],["typ",/^!\S*/,a,"!"],["str",/^"(?:[^"\\]|\\.)*(?:"|$)/,a,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,a,"'"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^\s+/,a," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\n\r]|$)/],["pun",/^-/],["kwd",/^\w+:[\n\r ]/],["pln",/^\w+/]]),["yaml","yml"]); ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/prettify.css ================================================ .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} ================================================ FILE: static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/prettify.js ================================================ !function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; (function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a= b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;ah[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com", /^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+ s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);p(C([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/], ["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);p(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);p(v({keywords:M,hashComments:!0,cStyleComments:!0,types:R}),["c","cc","cpp","cxx","cyc","m"]);p(v({keywords:"null,true,false"}),["json"]);p(v({keywords:O,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:R}), ["cs"]);p(v({keywords:N,cStyleComments:!0}),["java"]);p(v({keywords:y,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);p(v({keywords:P,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);p(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);p(v({keywords:Q, hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);p(v({keywords:E,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);p(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);p(v({keywords:W,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]); p(C([],[["str",/^[\S\s]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:C,registerLangHandler:p,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,g){var b=document.createElement("div");b.innerHTML="

    "+a+"
    ";b=b.firstChild;g&&J(b,g,!0);K({h:d,j:g,c:b,i:1}); return b.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function g(){for(var b=D.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;i=0;){var M=A[m],T=M.src.match(/^[^#?]*\/run_prettify\.js(\?[^#]*)?(?:#.*)?$/);if(T){z=T[1]||"";M.parentNode.removeChild(M);break}}var S=!0,D= [],N=[],K=[];z.replace(/[&?]([^&=]+)=([^&]+)/g,function(e,j,w){w=decodeURIComponent(w);j=decodeURIComponent(j);j=="autorun"?S=!/^[0fn]/i.test(w):j=="lang"?D.push(w):j=="skin"?N.push(w):j=="callback"&&K.push(w)});m=0;for(z=D.length;m122||(o<65||k>90||f.push([Math.max(65,k)|32,Math.min(o,90)|32]),o<97||k>122||f.push([Math.max(97,k)&-33,Math.min(o,122)&-33]))}}f.sort(function(f,a){return f[0]- a[0]||a[1]-f[1]});b=[];g=[];for(a=0;ak[0]&&(k[1]+1>k[0]&&c.push("-"),c.push(h(k[1])));c.push("]");return c.join("")}function e(f){for(var a=f.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],g=0,k=0;g=2&&f==="["?a[g]=b(o):f!=="\\"&&(a[g]=o.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var j=0,F=!1,l=!1,I=0,c=a.length;I=5&&"lang-"===y.substring(0,5))&&!(u&&typeof u[1]==="string"))g=!1,y="src";g||(m[B]=y)}k=c;c+=B.length;if(g){g=u[1];var o=B.indexOf(g),H=o+g.length;u[2]&&(H=B.length-u[2].length,o=H-g.length);y=y.substring(5);n(l+k,B.substring(0,o),h,j);n(l+k+o,g,A(y, g),j);n(l+k+H,B.substring(H),h,j)}else j.push(l+k,y)}a.g=j}var b={},e;(function(){for(var h=a.concat(d),l=[],i={},c=0,p=h.length;c=0;)b[q.charAt(f)]=m;m=m[1];q=""+m;i.hasOwnProperty(q)||(l.push(m),i[q]=r)}l.push(/[\S\s]/);e=j(l)})();var i=d.length;return h}function t(a){var d=[],h=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/, r,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,r,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,r,"\"'"]);a.verbatimStrings&&h.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,r]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,r,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/, r,"#"]),h.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,r])):d.push(["com",/^#[^\n\r]*/,r,"#"]));a.cStyleComments&&(h.push(["com",/^\/\/[^\n\r]*/,r]),h.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,r]));if(b=a.regexLiterals){var e=(b=b>1?"":"\n\r")?".":"[\\S\\s]";h.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+ ("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+e+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+e+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&h.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&h.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),r]);d.push(["pln",/^\s+/,r," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");h.push(["lit",/^@[$_a-z][\w$@]*/i,r],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,r],["pln",/^[$_a-z][\w$@]*/i,r],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i, r,"0123456789"],["pln",/^\\[\S\s]?/,r],["pun",RegExp(b),r]);return C(d,h)}function z(a,d,h){function b(a){var c=a.nodeType;if(c==1&&!j.test(a.className))if("br"===a.nodeName)e(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&h){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(l.createTextNode(d),a.nextSibling),e(a),c||a.parentNode.removeChild(a)}} function e(a){function b(a,c){var d=c?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),h=a.nextSibling;f.appendChild(d);for(var e=h;e;e=h)h=e.nextSibling,f.appendChild(e)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var j=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,l=a.ownerDocument,i=l.createElement("li");a.firstChild;)i.appendChild(a.firstChild);for(var c=[i],p=0;p=0;){var b=d[h];U.hasOwnProperty(b)?V.console&&console.warn("cannot override language handler %s",b):U[b]=a}}function A(a,d){if(!a||!U.hasOwnProperty(a))a=/^\s*=o&&(b+=2);h>=H&&(t+=2)}}finally{if(g)g.style.display=k}}catch(v){V.console&&console.log(v&&v.stack||v)}}var V=window,G=["break,continue,do,else,for,if,return,while"],O=[[G,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],J=[O,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],K=[O,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], L=[K,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],O=[O,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],M=[G,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], N=[G,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],R=[G,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],G=[G,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],Q=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, S=/\S/,T=t({keywords:[J,L,O,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",M,N,G],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),U={};i(T,["default-code"]);i(C([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);i(C([["pln",/^\s+/,r," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,r,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/], ["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);i(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);i(t({keywords:J,hashComments:!0,cStyleComments:!0,types:Q}),["c","cc","cpp","cxx","cyc","m"]);i(t({keywords:"null,true,false"}),["json"]);i(t({keywords:L,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:Q}), ["cs"]);i(t({keywords:K,cStyleComments:!0}),["java"]);i(t({keywords:G,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);i(t({keywords:M,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);i(t({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);i(t({keywords:N, hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);i(t({keywords:O,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);i(t({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);i(t({keywords:R,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]); i(C([],[["str",/^[\S\s]+/]]),["regex"]);var X=V.PR={createSimpleLexer:C,registerLangHandler:i,sourceDecorator:t,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:function(a,d,e){var b=document.createElement("div");b.innerHTML="
    "+a+"
    ";b=b.firstChild;e&&z(b,e,!0);D({h:d,j:e,c:b,i:1});return b.innerHTML}, prettyPrint:e=e=function(a,d){function e(){for(var b=V.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;p", "/": "?", "\\": "|" } }; function keyHandler( handleObj ) { // Only care when a possible input has been specified if ( typeof handleObj.data !== "string" ) { return; } var origHandler = handleObj.handler, keys = handleObj.data.toLowerCase().split(" "), textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"]; handleObj.handler = function( event ) { // Don't fire in text-accepting inputs that we didn't directly bind to if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1 ) ) { return; } // Keypress represents characters, not special keys var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], character = String.fromCharCode( event.which ).toLowerCase(), key, modif = "", possible = {}; // check combinations (alt|ctrl|shift+anything) if ( event.altKey && special !== "alt" ) { modif += "alt+"; } if ( event.ctrlKey && special !== "ctrl" ) { modif += "ctrl+"; } // TODO: Need to make sure this works consistently across platforms if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { modif += "meta+"; } if ( event.shiftKey && special !== "shift" ) { modif += "shift+"; } if ( special ) { possible[ modif + special ] = true; } else { possible[ modif + character ] = true; possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" if ( modif === "shift+" ) { possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; } } for ( var i = 0, l = keys.length; i < l; i++ ) { if ( possible[ keys[i] ] ) { return origHandler.apply( this, arguments ); } } }; } jQuery.each([ "keydown", "keyup", "keypress" ], function() { jQuery.event.special[ this ] = { add: keyHandler }; }); })( jQuery ); ================================================ FILE: static/bootstrap/plugins/tagsinput/bootstrap-tagsinput.css ================================================ .bootstrap-tagsinput { background-color: #fff; border: 1px solid #ccc; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); display: inline-block; padding: 4px 6px; color: #555; vertical-align: middle; border-radius: 4px; max-width: 100%; line-height: 22px; cursor: text; } .bootstrap-tagsinput input { border: none; box-shadow: none; outline: none; background-color: transparent; padding: 0 6px; margin: 0; width: auto; max-width: inherit; } .bootstrap-tagsinput.form-control input::-moz-placeholder { color: #777; opacity: 1; } .bootstrap-tagsinput.form-control input:-ms-input-placeholder { color: #777; } .bootstrap-tagsinput.form-control input::-webkit-input-placeholder { color: #777; } .bootstrap-tagsinput input:focus { border: none; box-shadow: none; } .bootstrap-tagsinput .tag { margin-right: 2px; color: white; } .bootstrap-tagsinput .tag [data-role="remove"] { margin-left: 8px; cursor: pointer; } .bootstrap-tagsinput .tag [data-role="remove"]:after { content: "x"; padding: 0px 2px; } .bootstrap-tagsinput .tag [data-role="remove"]:hover { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); } .bootstrap-tagsinput .tag [data-role="remove"]:hover:active { box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } ================================================ FILE: static/bootstrap/plugins/tagsinput/bootstrap-tagsinput.js ================================================ (function ($) { "use strict"; var defaultOptions = { tagClass: function(item) { return 'label label-info'; }, itemValue: function(item) { return item ? item.toString() : item; }, itemText: function(item) { return this.itemValue(item); }, itemTitle: function(item) { return null; }, freeInput: true, addOnBlur: true, maxTags: undefined, maxChars: undefined, confirmKeys: [13, 44], delimiter: ',', delimiterRegex: null, cancelConfirmKeysOnEmpty: true, onTagExists: function(item, $tag) { $tag.hide().fadeIn(); }, trimValue: false, allowDuplicates: false }; /** * Constructor function */ function TagsInput(element, options) { this.itemsArray = []; this.$element = $(element); this.$element.hide(); this.isSelect = (element.tagName === 'SELECT'); this.multiple = (this.isSelect && element.hasAttribute('multiple')); this.objectItems = options && options.itemValue; this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; this.inputSize = Math.max(1, this.placeholderText.length); this.$container = $('
    '); this.$input = $('').appendTo(this.$container); this.$element.before(this.$container); this.build(options); } TagsInput.prototype = { constructor: TagsInput, /** * Adds the given item as a new tag. Pass true to dontPushVal to prevent * updating the elements val() */ add: function(item, dontPushVal, options) { var self = this; if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) return; // Ignore falsey values, except false if (item !== false && !item) return; // Trim value if (typeof item === "string" && self.options.trimValue) { item = $.trim(item); } // Throw an error when trying to add an object while the itemValue option was not set if (typeof item === "object" && !self.objectItems) throw("Can't add objects when itemValue option is not set"); // Ignore strings only containg whitespace if (item.toString().match(/^\s*$/)) return; // If SELECT but not multiple, remove current tag if (self.isSelect && !self.multiple && self.itemsArray.length > 0) self.remove(self.itemsArray[0]); if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter; var items = item.split(delimiter); if (items.length > 1) { for (var i = 0; i < items.length; i++) { this.add(items[i], true); } if (!dontPushVal) self.pushVal(); return; } } var itemValue = self.options.itemValue(item), itemText = self.options.itemText(item), tagClass = self.options.tagClass(item), itemTitle = self.options.itemTitle(item); // Ignore items allready added var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; if (existing && !self.options.allowDuplicates) { // Invoke onTagExists if (self.options.onTagExists) { var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); self.options.onTagExists(item, $existingTag); } return; } // if length greater than limit if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) return; // raise beforeItemAdd arg var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options}); self.$element.trigger(beforeItemAddEvent); if (beforeItemAddEvent.cancel) return; // register item in internal array and map self.itemsArray.push(item); // add a tag element var $tag = $('' + htmlEncode(itemText) + ''); $tag.data('item', item); self.findInputWrapper().before($tag); $tag.after(' '); // add