├── .bowerrc
├── .editorconfig
├── .gitignore
├── .gitmodules
├── .jscsrc
├── .jshintrc
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── Gruntfile.js
├── LICENSE
├── PRIVACY.md
├── README.md
├── SECURITY.md
├── config.example.js
├── content
├── apps
│ └── README.md
├── data
│ └── README.md
├── images
│ └── README.md
└── storage
│ └── qiniu-store
│ └── index.js
├── core
├── client
│ ├── .ember-cli
│ ├── .gitignore
│ ├── .jshintrc
│ ├── Brocfile.js
│ ├── app
│ │ ├── README.md
│ │ ├── _config.yml
│ │ ├── adapters
│ │ │ ├── application.js
│ │ │ ├── base.js
│ │ │ ├── embedded-relation-adapter.js
│ │ │ ├── setting.js
│ │ │ └── user.js
│ │ ├── app.js
│ │ ├── assets
│ │ │ └── lib
│ │ │ │ └── uploader.js
│ │ ├── components
│ │ │ ├── gh-activating-list-item.js
│ │ │ ├── gh-blog-url.js
│ │ │ ├── gh-cm-editor.js
│ │ │ ├── gh-dropdown-button.js
│ │ │ ├── gh-dropdown.js
│ │ │ ├── gh-ed-editor.js
│ │ │ ├── gh-ed-preview.js
│ │ │ ├── gh-file-upload.js
│ │ │ ├── gh-form.js
│ │ │ ├── gh-input.js
│ │ │ ├── gh-modal-dialog.js
│ │ │ ├── gh-navitem-url-input.js
│ │ │ ├── gh-navitem.js
│ │ │ ├── gh-notification.js
│ │ │ ├── gh-notifications.js
│ │ │ ├── gh-popover-button.js
│ │ │ ├── gh-popover.js
│ │ │ ├── gh-role-selector.js
│ │ │ ├── gh-select.js
│ │ │ ├── gh-tab-pane.js
│ │ │ ├── gh-tab.js
│ │ │ ├── gh-tabs-manager.js
│ │ │ ├── gh-textarea.js
│ │ │ ├── gh-trim-focus-input.js
│ │ │ ├── gh-upload-modal.js
│ │ │ ├── gh-uploader.js
│ │ │ └── gh-url-preview.js
│ │ ├── controllers
│ │ │ ├── application.js
│ │ │ ├── editor
│ │ │ │ ├── edit.js
│ │ │ │ └── new.js
│ │ │ ├── error.js
│ │ │ ├── feature.js
│ │ │ ├── forgotten.js
│ │ │ ├── modals
│ │ │ │ ├── copy-html.js
│ │ │ │ ├── delete-all.js
│ │ │ │ ├── delete-post.js
│ │ │ │ ├── delete-tag.js
│ │ │ │ ├── delete-user.js
│ │ │ │ ├── invite-new-user.js
│ │ │ │ ├── leave-editor.js
│ │ │ │ ├── signin.js
│ │ │ │ ├── transfer-owner.js
│ │ │ │ └── upload.js
│ │ │ ├── post-settings-menu.js
│ │ │ ├── post-tags-input.js
│ │ │ ├── posts.js
│ │ │ ├── posts
│ │ │ │ └── post.js
│ │ │ ├── reset.js
│ │ │ ├── settings.js
│ │ │ ├── settings
│ │ │ │ ├── about.js
│ │ │ │ ├── app.js
│ │ │ │ ├── code-injection.js
│ │ │ │ ├── general.js
│ │ │ │ ├── labs.js
│ │ │ │ ├── navigation.js
│ │ │ │ ├── tags.js
│ │ │ │ └── users
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── user.js
│ │ │ ├── setup.js
│ │ │ ├── signin.js
│ │ │ └── signup.js
│ │ ├── helpers
│ │ │ ├── gh-count-characters.js
│ │ │ ├── gh-count-down-characters.js
│ │ │ ├── gh-count-words.js
│ │ │ ├── gh-format-html.js
│ │ │ ├── gh-format-markdown.js
│ │ │ ├── gh-format-timeago.js
│ │ │ └── gh-path.js
│ │ ├── html
│ │ │ ├── permalinks.html
│ │ │ └── scrollbars.html
│ │ ├── index.html
│ │ ├── initializers
│ │ │ ├── authentication.js
│ │ │ ├── dropdown.js
│ │ │ ├── ghost-config.js
│ │ │ ├── ghost-paths.js
│ │ │ ├── notifications.js
│ │ │ ├── store-injector.js
│ │ │ └── trailing-history.js
│ │ ├── mixins
│ │ │ ├── body-event-listener.js
│ │ │ ├── current-user-settings.js
│ │ │ ├── dropdown-mixin.js
│ │ │ ├── ed-editor-api.js
│ │ │ ├── ed-editor-scroll.js
│ │ │ ├── ed-editor-shortcuts.js
│ │ │ ├── editor-base-controller.js
│ │ │ ├── editor-base-route.js
│ │ │ ├── editor-base-view.js
│ │ │ ├── loading-indicator.js
│ │ │ ├── nprogress-save.js
│ │ │ ├── pagination-controller.js
│ │ │ ├── pagination-route.js
│ │ │ ├── pagination-view-infinite-scroll.js
│ │ │ ├── selective-save.js
│ │ │ ├── settings-menu-controller.js
│ │ │ ├── shortcuts-route.js
│ │ │ ├── style-body.js
│ │ │ ├── text-input.js
│ │ │ └── validation-engine.js
│ │ ├── models
│ │ │ ├── notification.js
│ │ │ ├── post.js
│ │ │ ├── role.js
│ │ │ ├── setting.js
│ │ │ ├── slug-generator.js
│ │ │ ├── tag.js
│ │ │ └── user.js
│ │ ├── router.js
│ │ ├── routes
│ │ │ ├── application.js
│ │ │ ├── authenticated.js
│ │ │ ├── content.js
│ │ │ ├── debug.js
│ │ │ ├── editor
│ │ │ │ ├── edit.js
│ │ │ │ ├── index.js
│ │ │ │ └── new.js
│ │ │ ├── error404.js
│ │ │ ├── forgotten.js
│ │ │ ├── mobile-index-route.js
│ │ │ ├── posts.js
│ │ │ ├── posts
│ │ │ │ ├── index.js
│ │ │ │ └── post.js
│ │ │ ├── reset.js
│ │ │ ├── settings.js
│ │ │ ├── settings
│ │ │ │ ├── about.js
│ │ │ │ ├── apps.js
│ │ │ │ ├── code-injection.js
│ │ │ │ ├── general.js
│ │ │ │ ├── index.js
│ │ │ │ ├── labs.js
│ │ │ │ ├── navigation.js
│ │ │ │ ├── tags.js
│ │ │ │ ├── users.js
│ │ │ │ └── users
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── user.js
│ │ │ ├── setup.js
│ │ │ ├── signin.js
│ │ │ ├── signout.js
│ │ │ └── signup.js
│ │ ├── serializers
│ │ │ ├── application.js
│ │ │ ├── post.js
│ │ │ ├── setting.js
│ │ │ ├── tag.js
│ │ │ └── user.js
│ │ ├── styles
│ │ │ ├── app.scss
│ │ │ ├── components
│ │ │ │ ├── badges.scss
│ │ │ │ ├── dropdowns.scss
│ │ │ │ ├── modals.scss
│ │ │ │ ├── navigation.scss
│ │ │ │ ├── notifications.scss
│ │ │ │ ├── pagination.scss
│ │ │ │ ├── popovers.scss
│ │ │ │ ├── settings-menu.scss
│ │ │ │ ├── splitbuttons.scss
│ │ │ │ ├── uploader.scss
│ │ │ │ └── url-preview.scss
│ │ │ ├── layouts
│ │ │ │ ├── about.scss
│ │ │ │ ├── auth.scss
│ │ │ │ ├── content.scss
│ │ │ │ ├── default.scss
│ │ │ │ ├── editor.scss
│ │ │ │ ├── error.scss
│ │ │ │ ├── settings.scss
│ │ │ │ ├── setup.scss
│ │ │ │ ├── tags.scss
│ │ │ │ ├── user.scss
│ │ │ │ └── users.scss
│ │ │ ├── modules
│ │ │ │ ├── animations.scss
│ │ │ │ ├── icons.scss
│ │ │ │ ├── mixins.scss
│ │ │ │ └── variables.scss
│ │ │ ├── patterns
│ │ │ │ ├── _shame.scss
│ │ │ │ ├── buttons.scss
│ │ │ │ ├── forms.scss
│ │ │ │ ├── global.scss
│ │ │ │ ├── labels.scss
│ │ │ │ ├── navlist.scss
│ │ │ │ └── tables.scss
│ │ │ └── vendor
│ │ │ │ ├── nanoscroller.scss
│ │ │ │ └── nprogress.scss
│ │ ├── templates
│ │ │ ├── -import-errors.hbs
│ │ │ ├── -navbar.hbs
│ │ │ ├── -publish-bar.hbs
│ │ │ ├── -user-actions-menu.hbs
│ │ │ ├── application.hbs
│ │ │ ├── components
│ │ │ │ ├── gh-activating-list-item.hbs
│ │ │ │ ├── gh-blog-url.hbs
│ │ │ │ ├── gh-ed-preview.hbs
│ │ │ │ ├── gh-file-upload.hbs
│ │ │ │ ├── gh-modal-dialog.hbs
│ │ │ │ ├── gh-navitem.hbs
│ │ │ │ ├── gh-notification.hbs
│ │ │ │ ├── gh-notifications.hbs
│ │ │ │ ├── gh-role-selector.hbs
│ │ │ │ ├── gh-uploader.hbs
│ │ │ │ └── gh-url-preview.hbs
│ │ │ ├── editor-save-button.hbs
│ │ │ ├── editor
│ │ │ │ └── edit.hbs
│ │ │ ├── error.hbs
│ │ │ ├── forgotten.hbs
│ │ │ ├── modals
│ │ │ │ ├── copy-html.hbs
│ │ │ │ ├── delete-all.hbs
│ │ │ │ ├── delete-post.hbs
│ │ │ │ ├── delete-tag.hbs
│ │ │ │ ├── delete-user.hbs
│ │ │ │ ├── invite-new-user.hbs
│ │ │ │ ├── leave-editor.hbs
│ │ │ │ ├── markdown.hbs
│ │ │ │ ├── signin.hbs
│ │ │ │ ├── transfer-owner.hbs
│ │ │ │ └── upload.hbs
│ │ │ ├── post-settings-menu.hbs
│ │ │ ├── post-tags-input.hbs
│ │ │ ├── posts.hbs
│ │ │ ├── posts
│ │ │ │ ├── index.hbs
│ │ │ │ └── post.hbs
│ │ │ ├── reset.hbs
│ │ │ ├── settings.hbs
│ │ │ ├── settings
│ │ │ │ ├── about.hbs
│ │ │ │ ├── apps.hbs
│ │ │ │ ├── code-injection.hbs
│ │ │ │ ├── general.hbs
│ │ │ │ ├── labs.hbs
│ │ │ │ ├── navigation.hbs
│ │ │ │ ├── tags.hbs
│ │ │ │ ├── tags
│ │ │ │ │ └── settings-menu.hbs
│ │ │ │ ├── users.hbs
│ │ │ │ └── users
│ │ │ │ │ ├── index.hbs
│ │ │ │ │ └── user.hbs
│ │ │ ├── setup.hbs
│ │ │ ├── signin.hbs
│ │ │ └── signup.hbs
│ │ ├── transforms
│ │ │ └── moment-date.js
│ │ ├── utils
│ │ │ ├── ajax.js
│ │ │ ├── bind.js
│ │ │ ├── bound-one-way.js
│ │ │ ├── caja-sanitizers.js
│ │ │ ├── config-parser.js
│ │ │ ├── ctrl-or-cmd.js
│ │ │ ├── date-formatting.js
│ │ │ ├── document-title.js
│ │ │ ├── dropdown-service.js
│ │ │ ├── ed-image-manager.js
│ │ │ ├── editor-shortcuts.js
│ │ │ ├── ghost-paths.js
│ │ │ ├── isFinite.js
│ │ │ ├── isNumber.js
│ │ │ ├── link-view.js
│ │ │ ├── mobile.js
│ │ │ ├── notifications.js
│ │ │ ├── random-password.js
│ │ │ ├── set-scroll-classname.js
│ │ │ ├── text-field.js
│ │ │ ├── titleize.js
│ │ │ ├── validator-extensions.js
│ │ │ └── word-count.js
│ │ ├── validators
│ │ │ ├── forgotten.js
│ │ │ ├── new-user.js
│ │ │ ├── post.js
│ │ │ ├── reset.js
│ │ │ ├── setting.js
│ │ │ ├── setup.js
│ │ │ ├── signin.js
│ │ │ ├── signup.js
│ │ │ ├── tag-settings.js
│ │ │ └── user.js
│ │ └── views
│ │ │ ├── application.js
│ │ │ ├── content-preview-content-view.js
│ │ │ ├── editor-save-button.js
│ │ │ ├── editor
│ │ │ ├── edit.js
│ │ │ └── new.js
│ │ │ ├── mobile
│ │ │ ├── content-view.js
│ │ │ ├── index-view.js
│ │ │ └── parent-view.js
│ │ │ ├── paginated-scroll-box.js
│ │ │ ├── post-item-view.js
│ │ │ ├── post-tags-input.js
│ │ │ ├── posts.js
│ │ │ ├── posts
│ │ │ ├── index.js
│ │ │ └── post.js
│ │ │ ├── settings.js
│ │ │ └── settings
│ │ │ ├── about.js
│ │ │ ├── apps.js
│ │ │ ├── code-injection.js
│ │ │ ├── content-base.js
│ │ │ ├── general.js
│ │ │ ├── index.js
│ │ │ ├── labs.js
│ │ │ ├── navigation.js
│ │ │ ├── pass-protect.js
│ │ │ ├── tags.js
│ │ │ ├── tags
│ │ │ └── settings-menu.js
│ │ │ ├── users.js
│ │ │ └── users
│ │ │ ├── user.js
│ │ │ └── users-list-view.js
│ ├── bower.json
│ ├── config
│ │ └── environment.js
│ ├── lib
│ │ ├── .jshintrc
│ │ └── asset-delivery
│ │ │ ├── index.js
│ │ │ └── package.json
│ ├── package.json
│ ├── public
│ │ └── assets
│ │ │ ├── fonts
│ │ │ ├── icons.svg
│ │ │ └── icons.woff
│ │ │ └── img
│ │ │ ├── 404-ghost.png
│ │ │ ├── 404-ghost@2x.png
│ │ │ ├── large.png
│ │ │ ├── loadingcat.gif
│ │ │ ├── medium.png
│ │ │ ├── small.png
│ │ │ ├── touch-icon-ipad.png
│ │ │ └── touch-icon-iphone.png
│ ├── testem.json
│ └── tests
│ │ ├── .jshintrc
│ │ ├── helpers
│ │ ├── resolver.js
│ │ └── start-app.js
│ │ ├── index.html
│ │ ├── test-helper.js
│ │ └── unit
│ │ ├── .gitkeep
│ │ ├── components
│ │ ├── gh-trim-focus-input_test.js
│ │ └── gh-url-preview_test.js
│ │ ├── controllers
│ │ ├── post-settings-menu_test.js
│ │ └── settings-general_test.js
│ │ ├── models
│ │ ├── post_test.js
│ │ ├── role_test.js
│ │ ├── setting_test.js
│ │ ├── tag_test.js
│ │ └── user_test.js
│ │ └── utils
│ │ └── ghost-paths_test.js
├── index.js
├── server
│ ├── api
│ │ ├── authentication.js
│ │ ├── configuration.js
│ │ ├── db.js
│ │ ├── index.js
│ │ ├── mail.js
│ │ ├── notifications.js
│ │ ├── posts.js
│ │ ├── roles.js
│ │ ├── settings.js
│ │ ├── slugs.js
│ │ ├── tags.js
│ │ ├── themes.js
│ │ ├── upload.js
│ │ ├── users.js
│ │ └── utils.js
│ ├── apps
│ │ ├── dependencies.js
│ │ ├── index.js
│ │ ├── loader.js
│ │ ├── permissions.js
│ │ ├── proxy.js
│ │ └── sandbox.js
│ ├── config
│ │ ├── index.js
│ │ └── url.js
│ ├── controllers
│ │ ├── admin.js
│ │ └── frontend.js
│ ├── data
│ │ ├── default-settings.json
│ │ ├── export
│ │ │ └── index.js
│ │ ├── fixtures
│ │ │ ├── fixtures.json
│ │ │ ├── index.js
│ │ │ └── permissions
│ │ │ │ ├── index.js
│ │ │ │ └── permissions.json
│ │ ├── import
│ │ │ ├── data-importer.js
│ │ │ ├── index.js
│ │ │ └── utils.js
│ │ ├── importer
│ │ │ ├── handlers
│ │ │ │ ├── image.js
│ │ │ │ ├── json.js
│ │ │ │ └── markdown.js
│ │ │ ├── importers
│ │ │ │ ├── data.js
│ │ │ │ └── image.js
│ │ │ └── index.js
│ │ ├── migration
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── schema.js
│ │ ├── utils
│ │ │ ├── clients
│ │ │ │ ├── index.js
│ │ │ │ ├── mysql.js
│ │ │ │ ├── pg.js
│ │ │ │ └── sqlite3.js
│ │ │ └── index.js
│ │ ├── validation
│ │ │ └── index.js
│ │ ├── versioning
│ │ │ └── index.js
│ │ └── xml
│ │ │ ├── rss
│ │ │ └── index.js
│ │ │ ├── sitemap
│ │ │ ├── base-generator.js
│ │ │ ├── handler.js
│ │ │ ├── index-generator.js
│ │ │ ├── index.js
│ │ │ ├── manager.js
│ │ │ ├── page-generator.js
│ │ │ ├── post-generator.js
│ │ │ ├── tag-generator.js
│ │ │ ├── user-generator.js
│ │ │ └── utils.js
│ │ │ └── xmlrpc.js
│ ├── email-templates
│ │ ├── invite-user.html
│ │ ├── raw
│ │ │ ├── invite-user.html
│ │ │ ├── reset-password.html
│ │ │ ├── test.html
│ │ │ └── welcome.html
│ │ ├── reset-password.html
│ │ ├── test.html
│ │ └── welcome.html
│ ├── errors
│ │ ├── bad-request-error.js
│ │ ├── data-import-error.js
│ │ ├── email-error.js
│ │ ├── index.js
│ │ ├── internal-server-error.js
│ │ ├── no-permission-error.js
│ │ ├── not-found-error.js
│ │ ├── request-too-large-error.js
│ │ ├── unauthorized-error.js
│ │ ├── unsupported-media-type-error.js
│ │ └── validation-error.js
│ ├── events
│ │ └── index.js
│ ├── filters.js
│ ├── ghost-server.js
│ ├── helpers
│ │ ├── asset.js
│ │ ├── author.js
│ │ ├── body_class.js
│ │ ├── content.js
│ │ ├── date.js
│ │ ├── encode.js
│ │ ├── excerpt.js
│ │ ├── foreach.js
│ │ ├── ghost_foot.js
│ │ ├── ghost_head.js
│ │ ├── has.js
│ │ ├── image.js
│ │ ├── index.js
│ │ ├── input_password.js
│ │ ├── is.js
│ │ ├── meta_description.js
│ │ ├── meta_title.js
│ │ ├── navigation.js
│ │ ├── page_url.js
│ │ ├── pagination.js
│ │ ├── plural.js
│ │ ├── post_class.js
│ │ ├── prev_next.js
│ │ ├── tags.js
│ │ ├── template.js
│ │ ├── title.js
│ │ ├── tpl
│ │ │ ├── navigation.hbs
│ │ │ └── pagination.hbs
│ │ ├── url.js
│ │ └── utils.js
│ ├── index.js
│ ├── mail.js
│ ├── middleware
│ │ ├── auth-strategies.js
│ │ ├── ghost-busboy.js
│ │ ├── index.js
│ │ ├── middleware.js
│ │ └── oauth.js
│ ├── models
│ │ ├── accesstoken.js
│ │ ├── app-field.js
│ │ ├── app-setting.js
│ │ ├── app.js
│ │ ├── base.js
│ │ ├── basetoken.js
│ │ ├── client.js
│ │ ├── index.js
│ │ ├── permission.js
│ │ ├── post.js
│ │ ├── refreshtoken.js
│ │ ├── role.js
│ │ ├── settings.js
│ │ ├── tag.js
│ │ └── user.js
│ ├── permissions
│ │ ├── effective.js
│ │ └── index.js
│ ├── require-tree.js
│ ├── routes
│ │ ├── admin.js
│ │ ├── api.js
│ │ ├── frontend.js
│ │ └── index.js
│ ├── storage
│ │ ├── base.js
│ │ ├── index.js
│ │ └── local-file-store.js
│ ├── update-check.js
│ ├── utils
│ │ ├── downzero.js
│ │ ├── index.js
│ │ ├── pipeline.js
│ │ ├── sequence.js
│ │ └── startup-check.js
│ └── views
│ │ ├── error.hbs
│ │ ├── private.hbs
│ │ └── user-error.hbs
├── shared
│ ├── favicon.ico
│ ├── img
│ │ ├── user-cover.png
│ │ └── user-image.png
│ ├── private-robots.txt
│ ├── robots.txt
│ └── sitemap.xsl
└── test
│ ├── .jshintrc
│ ├── functional
│ ├── base.js
│ ├── client
│ │ ├── app_test.js
│ │ ├── content_test.js
│ │ ├── editor_test.js
│ │ ├── psm_test.js
│ │ ├── settings_about_test.js
│ │ ├── settings_test.js
│ │ ├── signin_test.js
│ │ ├── signout_test.js
│ │ └── signup_test.js
│ ├── module
│ │ └── module_spec.js
│ ├── routes
│ │ ├── admin_spec.js
│ │ ├── api
│ │ │ ├── authentication_spec.js
│ │ │ ├── db_spec.js
│ │ │ ├── error_spec.js
│ │ │ ├── notifications_spec.js
│ │ │ ├── posts_spec.js
│ │ │ ├── settings_spec.js
│ │ │ ├── slugs_spec.js
│ │ │ ├── tags_spec.js
│ │ │ └── users_spec.js
│ │ └── frontend_spec.js
│ └── setup
│ │ └── setup_test.js
│ ├── integration
│ ├── api
│ │ ├── api_authentication_spec.js
│ │ ├── api_configuration_spec.js
│ │ ├── api_db_spec.js
│ │ ├── api_mail_spec.js
│ │ ├── api_notifications_spec.js
│ │ ├── api_posts_spec.js
│ │ ├── api_roles_spec.js
│ │ ├── api_settings_spec.js
│ │ ├── api_slugs_spec.js
│ │ ├── api_tags_spec.js
│ │ ├── api_themes_spec.js
│ │ ├── api_upload_spec.js
│ │ └── api_users_spec.js
│ ├── export_spec.js
│ ├── import_spec.js
│ ├── model
│ │ ├── model_app-fields_spec.js
│ │ ├── model_app-settings_spec.js
│ │ ├── model_apps_spec.js
│ │ ├── model_permissions_spec.js
│ │ ├── model_posts_spec.js
│ │ ├── model_roles_spec.js
│ │ ├── model_settings_spec.js
│ │ ├── model_tags_spec.js
│ │ └── model_users_spec.js
│ └── update_check_spec.js
│ ├── unit
│ ├── apps_spec.js
│ ├── config_spec.js
│ ├── error_handling_spec.js
│ ├── filters_spec.js
│ ├── frontend_spec.js
│ ├── importer_spec.js
│ ├── mail_spec.js
│ ├── middleware_spec.js
│ ├── migration_spec.js
│ ├── permissions_spec.js
│ ├── server_helpers
│ │ ├── asset_spec.js
│ │ ├── author_spec.js
│ │ ├── body_class_spec.js
│ │ ├── content_spec.js
│ │ ├── date_spec.js
│ │ ├── encode_spec.js
│ │ ├── excerpt_spec.js
│ │ ├── foreach_spec.js
│ │ ├── ghost_foot_spec.js
│ │ ├── ghost_head_spec.js
│ │ ├── has_spec.js
│ │ ├── image_spec.js
│ │ ├── input_password_spec.js
│ │ ├── is_spec.js
│ │ ├── meta_description_spec.js
│ │ ├── meta_title_spec.js
│ │ ├── navigation_spec.js
│ │ ├── next_post_spec.js
│ │ ├── page_url_spec.js
│ │ ├── pagination_spec.js
│ │ ├── plural_spec.js
│ │ ├── post_class_spec.js
│ │ ├── prev_post_spec.js
│ │ ├── tags_spec.js
│ │ ├── test_tpl
│ │ │ ├── navigation.hbs
│ │ │ └── pagination.hbs
│ │ ├── title_spec.js
│ │ ├── url_spec.js
│ │ └── utils.js
│ ├── server_helpers_index_spec.js
│ ├── server_helpers_template_spec.js
│ ├── server_spec.js
│ ├── showdown_client_integrated_spec.js
│ ├── sitemap_spec.js
│ ├── storage_local-file-store_spec.js
│ └── xmlrpc_spec.js
│ └── utils
│ ├── api.js
│ ├── fixtures
│ ├── app
│ │ ├── badinstall.js
│ │ ├── badlib.js
│ │ ├── badoutside.js
│ │ ├── badrequire.js
│ │ ├── badtop.js
│ │ ├── good.js
│ │ ├── goodlib.js
│ │ └── nested
│ │ │ └── goodnested.js
│ ├── data-generator.js
│ ├── example.js
│ ├── export
│ │ ├── export-000.json
│ │ ├── export-001.json
│ │ ├── export-002.json
│ │ ├── export-003-api-wrapper-bad.json
│ │ ├── export-003-api-wrapper.json
│ │ ├── export-003-badValidation.json
│ │ ├── export-003-dbErrors.json
│ │ ├── export-003-duplicate-posts.json
│ │ ├── export-003-duplicate-tags.json
│ │ ├── export-003-minimal.json
│ │ ├── export-003-mu-noOwner.json
│ │ ├── export-003-mu-unknownAuthor.json
│ │ ├── export-003-mu.json
│ │ ├── export-003-nullPosts.json
│ │ ├── export-003-nullTags.json
│ │ ├── export-003-wrongUUID.json
│ │ └── export-003.json
│ ├── import
│ │ ├── deleted-2014-12-19-test-1.md
│ │ ├── draft-2014-12-19-test-1.md
│ │ ├── draft-2014-12-19-test-2.md
│ │ ├── draft-2014-12-19-test-3.md
│ │ ├── import-data-1.json
│ │ ├── published-2014-12-19-test-1.md
│ │ └── zips
│ │ │ ├── zip-image-dir
│ │ │ └── images
│ │ │ │ └── image.jpg
│ │ │ ├── zip-old-roon-export
│ │ │ └── Roon-Export
│ │ │ │ └── published
│ │ │ │ └── test.md
│ │ │ ├── zip-with-base-dir
│ │ │ └── basedir
│ │ │ │ └── test.json
│ │ │ ├── zip-with-double-base-dir
│ │ │ └── basedir
│ │ │ │ └── basedir
│ │ │ │ └── test.json
│ │ │ └── zip-without-base-dir
│ │ │ └── test.json
│ ├── test.hbs
│ └── theme
│ │ └── partials
│ │ └── test.hbs
│ ├── fork.js
│ ├── index.js
│ └── jscs-rules
│ └── disallow-object-controller.js
├── index.js
└── package.json
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "cwd": "core/client/",
3 | "directory": "bower_components"
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 4
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.hbs]
14 | insert_final_newline = false
15 |
16 | [{package,bower}.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [*.yml]
23 | indent_size = 2
24 |
25 | [Makefile]
26 | indent_style = tab
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | b-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 |
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | node_modules
16 | bower_components
17 | docs
18 | .bowerrc
19 | .idea/*
20 | *.iml
21 | *.sublime-*
22 | projectFilesBackup
23 |
24 | .DS_Store
25 |
26 | # vim-related
27 | [._]*.s[a-w][a-z]
28 | [._]s[a-w][a-z]
29 | *.un~
30 | Session.vim
31 | .netrwhist
32 | .vimrc
33 | *~
34 |
35 | # TernJS
36 | .tern-project
37 |
38 | # Ghost DB file
39 | *.db
40 | *.db-journal
41 |
42 | .build
43 | .dist
44 | .tmp
45 |
46 | /core/server/data/export/exported*
47 | /content/tmp/*
48 | /content/data/*.db
49 | /content/apps/**/*
50 | /content/themes/**/*
51 | /content/images/**/*
52 | !/content/themes/casper/**
53 | !/README.md
54 |
55 | # Changelog, which is autogenerated, not committed
56 | CHANGELOG.md
57 |
58 | # Test generated files
59 | /core/test/functional/*.png
60 | /core/test/coverage
61 |
62 | config.js
63 | /core/client/config.js
64 |
65 | # Built asset files
66 | /core/built
67 | /core/server/views/default.hbs
68 |
69 | # Coverage reports
70 | coverage.html
71 |
72 | sftp-config.json
73 | handbook.md
74 |
75 | npm-shrinkwrap.json
76 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "content/themes/casper"]
2 | path = content/themes/casper
3 | url = https://github.com/diancloud/Casper.git
4 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": false,
4 | "strict": false,
5 | "sub": true,
6 | "eqeqeq": true,
7 | "laxbreak": true,
8 | "bitwise": true,
9 | "curly": true,
10 | "forin": true,
11 | "immed": true,
12 | "latedef": true,
13 | "newcap": true,
14 | "noarg": true,
15 | "noempty": true,
16 | "nonew": true,
17 | "plusplus": true,
18 | "regexp": true,
19 | "undef": true,
20 | "unused": true,
21 | "indent": 4,
22 | "predef": [ "-Promise" ]
23 | }
24 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | !**
2 | .build
3 | .dist
4 | .tmp
5 | docs/**
6 | _site/**
7 | content/images/**
8 | !content/images/README.md
9 | content/themes/**
10 | !content/themes/casper/**
11 | content/apps/**
12 | !content/apps/README.md
13 | content/data/**
14 | !content/data/README.md
15 | node_modules/**
16 | **/*.db*
17 | *.db*
18 | .sass*
19 | .af*
20 | .git*
21 | .groc*
22 | .jshintrc
23 | *.iml
24 | config.js
25 | core/built/**/*.map
26 | core/client/**
27 | core/test/**
28 | CONTRIBUTING.md
29 | SECURITY.md
30 | .travis.yml
31 | *.html
32 | !core/server/email-templates/**
33 | bower_components/**
34 | .editorconfig
35 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | - "0.12"
5 | - "iojs-v1.2.0"
6 | sudo: false
7 | cache:
8 | directories:
9 | - node_modules
10 | - bower_components
11 | addons:
12 | postgresql: "9.3"
13 | env:
14 | global:
15 | - GITHUB_OAUTH_KEY=003a44d58f12089d0c0261338298af3813330949
16 | matrix:
17 | - DB=sqlite3 NODE_ENV=testing
18 | - DB=mysql NODE_ENV=testing-mysql
19 | - DB=pg NODE_ENV=testing-pg
20 | before_install:
21 | - git clone git://github.com/n1k0/casperjs.git ~/casperjs
22 | - cd ~/casperjs
23 | - git checkout tags/1.1-beta3
24 | - export PATH=$PATH:`pwd`/bin
25 | - cd -
26 | - if [ $DB == "mysql" ]; then mysql -e 'create database ghost_testing'; fi
27 | - if [ $DB == "pg" ]; then psql -c 'create database ghost_testing;' -U postgres; fi
28 | before_script:
29 | - phantomjs --version
30 | - casperjs --version
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2015 Ghost Foundation
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Security Issues
2 |
3 | If you discover a security issue in Ghost, please report it by sending an email to security[at]ghost[dot]org
4 |
5 | This will allow us to assess the risk, and make a fix available before we add a bug report to the GitHub repository.
6 |
7 | Thanks for helping make Ghost safe for everyone.
8 |
--------------------------------------------------------------------------------
/content/apps/README.md:
--------------------------------------------------------------------------------
1 | # Content / Apps
2 |
3 | Coming soon, Ghost apps will appear here.
--------------------------------------------------------------------------------
/content/data/README.md:
--------------------------------------------------------------------------------
1 | # Content / Data
2 |
3 | If using the standard file storage, Ghost will save default db here.
--------------------------------------------------------------------------------
/content/images/README.md:
--------------------------------------------------------------------------------
1 | # Content / Images
2 |
3 | If using the standard file storage, Ghost will upload images to this directory.
--------------------------------------------------------------------------------
/core/client/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": true
9 | }
10 |
--------------------------------------------------------------------------------
/core/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # misc
12 | /.sass-cache
13 | /connect.lock
14 | /coverage/*
15 | /libpeerconnection.log
16 | npm-debug.log
17 | testem.log
18 |
19 | # built by grunt
20 | public/assets/img/contributors/
21 | app/templates/-contributors.hbs
22 |
--------------------------------------------------------------------------------
/core/client/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "-Promise",
6 | "-Notification",
7 | "$",
8 | "validator",
9 | "ic",
10 | "SimpleAuth",
11 | "NProgress",
12 | "moment"
13 | ],
14 | "browser": true,
15 | "boss": true,
16 | "curly": true,
17 | "debug": false,
18 | "devel": true,
19 | "eqeqeq": true,
20 | "evil": true,
21 | "forin": false,
22 | "immed": false,
23 | "laxbreak": false,
24 | "newcap": true,
25 | "noarg": true,
26 | "noempty": false,
27 | "nonew": false,
28 | "nomen": false,
29 | "onevar": false,
30 | "plusplus": false,
31 | "regexp": false,
32 | "undef": true,
33 | "sub": true,
34 | "strict": false,
35 | "white": false,
36 | "eqnull": true,
37 | "esnext": true,
38 | "unused": true
39 | }
40 |
--------------------------------------------------------------------------------
/core/client/app/_config.yml:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | markdown: kramdown
3 | highlighter: highlighter
4 |
5 | # Permalinks
6 | permalink: pretty
7 |
8 | # Server
9 | source: docs
10 | destination: _gh_pages
11 | host: 0.0.0.0
12 | port: 9001
13 | baseurl:
14 | url: http://localhost:9001
15 | encoding: UTF-8
16 |
--------------------------------------------------------------------------------
/core/client/app/adapters/application.js:
--------------------------------------------------------------------------------
1 | import EmbeddedRelationAdapter from 'ghost/adapters/embedded-relation-adapter';
2 |
3 | var ApplicationAdapter = EmbeddedRelationAdapter.extend();
4 |
5 | export default ApplicationAdapter;
6 |
--------------------------------------------------------------------------------
/core/client/app/adapters/base.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 | import ghostPaths from 'ghost/utils/ghost-paths';
3 |
4 | var BaseAdapter = DS.RESTAdapter.extend({
5 | host: window.location.origin,
6 | namespace: ghostPaths().apiRoot.slice(1),
7 |
8 | findQuery: function (store, type, query) {
9 | var id;
10 |
11 | if (query.id) {
12 | id = query.id;
13 | delete query.id;
14 | }
15 |
16 | return this.ajax(this.buildURL(type.typeKey, id), 'GET', {data: query});
17 | },
18 |
19 | buildURL: function (type, id) {
20 | // Ensure trailing slashes
21 | var url = this._super(type, id);
22 |
23 | if (url.slice(-1) !== '/') {
24 | url += '/';
25 | }
26 |
27 | return url;
28 | },
29 |
30 | // Override deleteRecord to disregard the response body on 2xx responses.
31 | // This is currently needed because the API is returning status 200 along
32 | // with the JSON object for the deleted entity and Ember expects an empty
33 | // response body for successful DELETEs.
34 | // Non-2xx (failure) responses will still work correctly as Ember will turn
35 | // them into rejected promises.
36 | deleteRecord: function () {
37 | var response = this._super.apply(this, arguments);
38 |
39 | return response.then(function () {
40 | return null;
41 | });
42 | }
43 | });
44 |
45 | export default BaseAdapter;
46 |
--------------------------------------------------------------------------------
/core/client/app/adapters/setting.js:
--------------------------------------------------------------------------------
1 | import ApplicationAdapter from 'ghost/adapters/application';
2 |
3 | var SettingAdapter = ApplicationAdapter.extend({
4 | updateRecord: function (store, type, record) {
5 | var data = {},
6 | serializer = store.serializerFor(type.typeKey);
7 |
8 | // remove the fake id that we added onto the model.
9 | delete record.id;
10 |
11 | // use the SettingSerializer to transform the model back into
12 | // an array of settings objects like the API expects
13 | serializer.serializeIntoHash(data, type, record);
14 |
15 | // use the ApplicationAdapter's buildURL method but do not
16 | // pass in an id.
17 | return this.ajax(this.buildURL(type.typeKey), 'PUT', {data: data});
18 | }
19 | });
20 |
21 | export default SettingAdapter;
22 |
--------------------------------------------------------------------------------
/core/client/app/adapters/user.js:
--------------------------------------------------------------------------------
1 | import ApplicationAdapter from 'ghost/adapters/application';
2 |
3 | var UserAdapter = ApplicationAdapter.extend({
4 | find: function (store, type, id) {
5 | return this.findQuery(store, type, {id: id, status: 'all'});
6 | }
7 | });
8 |
9 | export default UserAdapter;
10 |
--------------------------------------------------------------------------------
/core/client/app/app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Resolver from 'ember/resolver';
3 | import loadInitializers from 'ember/load-initializers';
4 | import 'ghost/utils/link-view';
5 | import 'ghost/utils/text-field';
6 | import config from './config/environment';
7 |
8 | Ember.MODEL_FACTORY_INJECTIONS = true;
9 |
10 | var App = Ember.Application.extend({
11 | modulePrefix: config.modulePrefix,
12 | podModulePrefix: config.podModulePrefix,
13 | Resolver: Resolver
14 | });
15 |
16 | loadInitializers(App, config.modulePrefix);
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-activating-list-item.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var ActivatingListItem = Ember.Component.extend({
3 | tagName: 'li',
4 | classNameBindings: ['active'],
5 | active: false,
6 |
7 | unfocusLink: function () {
8 | this.$('a').blur();
9 | }.on('click')
10 | });
11 |
12 | export default ActivatingListItem;
13 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-blog-url.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | var blogUrl = Ember.Component.extend({
4 | tagName: ''
5 | });
6 |
7 | export default blogUrl;
8 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-dropdown-button.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import DropdownMixin from 'ghost/mixins/dropdown-mixin';
3 |
4 | var DropdownButton = Ember.Component.extend(DropdownMixin, {
5 | tagName: 'button',
6 | attributeBindings: 'role',
7 | role: 'button',
8 |
9 | // matches with the dropdown this button toggles
10 | dropdownName: null,
11 |
12 | // Notify dropdown service this dropdown should be toggled
13 | click: function (event) {
14 | this._super(event);
15 | this.get('dropdown').toggleDropdown(this.get('dropdownName'), this);
16 | }
17 | });
18 |
19 | export default DropdownButton;
20 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-file-upload.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var FileUpload = Ember.Component.extend({
3 | _file: null,
4 |
5 | uploadButtonText: 'Text',
6 |
7 | uploadButtonDisabled: true,
8 |
9 | change: function (event) {
10 | this.set('uploadButtonDisabled', false);
11 | this.sendAction('onAdd');
12 | this._file = event.target.files[0];
13 | },
14 |
15 | onUpload: 'onUpload',
16 |
17 | actions: {
18 | upload: function () {
19 | if (!this.uploadButtonDisabled && this._file) {
20 | this.sendAction('onUpload', this._file);
21 | }
22 |
23 | // Prevent double post by disabling the button.
24 | this.set('uploadButtonDisabled', true);
25 | }
26 | }
27 | });
28 |
29 | export default FileUpload;
30 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-form.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var Form = Ember.View.extend({
3 | tagName: 'form',
4 | attributeBindings: ['enctype'],
5 | reset: function () {
6 | this.$().get(0).reset();
7 | },
8 | didInsertElement: function () {
9 | this.get('controller').on('reset', this, this.reset);
10 | },
11 | willClearRender: function () {
12 | this.get('controller').off('reset', this, this.reset);
13 | }
14 | });
15 |
16 | export default Form;
17 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-input.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import TextInputMixin from 'ghost/mixins/text-input';
3 |
4 | var Input = Ember.TextField.extend(TextInputMixin);
5 |
6 | export default Input;
7 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-navitem.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var NavItemComponent = Ember.Component.extend({
3 | classNames: 'navigation-item',
4 |
5 | attributeBindings: ['order:data-order'],
6 | order: Ember.computed.readOnly('navItem.order'),
7 |
8 | keyPress: function (event) {
9 | // enter key
10 | if (event.keyCode === 13) {
11 | event.preventDefault();
12 | this.get('controller').send('addItem');
13 | }
14 | },
15 |
16 | actions: {
17 | addItem: function () {
18 | this.sendAction('addItem');
19 | },
20 |
21 | deleteItem: function (item) {
22 | this.sendAction('deleteItem', item);
23 | },
24 |
25 | updateUrl: function (value) {
26 | this.sendAction('updateUrl', value, this.get('navItem'));
27 | }
28 | }
29 | });
30 |
31 | export default NavItemComponent;
32 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-notifications.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var NotificationsComponent = Ember.Component.extend({
3 | tagName: 'aside',
4 | classNames: 'notifications',
5 | classNameBindings: ['location'],
6 |
7 | messages: Ember.computed.filter('notifications', function (notification) {
8 | // If this instance of the notifications component has no location affinity
9 | // then it gets all notifications
10 | if (!this.get('location')) {
11 | return true;
12 | }
13 |
14 | var displayLocation = (typeof notification.toJSON === 'function') ?
15 | notification.get('location') : notification.location;
16 |
17 | return this.get('location') === displayLocation;
18 | }),
19 |
20 | messageCountObserver: function () {
21 | this.sendAction('notify', this.get('messages').length);
22 | }.observes('messages.[]')
23 | });
24 |
25 | export default NotificationsComponent;
26 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-popover-button.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import DropdownButton from 'ghost/components/gh-dropdown-button';
3 |
4 | var PopoverButton = DropdownButton.extend({
5 | click: Ember.K, // We don't want clicks on popovers, but dropdowns have them. So `K`ill them here.
6 |
7 | mouseEnter: function (event) {
8 | this._super(event);
9 | this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
10 | },
11 |
12 | mouseLeave: function (event) {
13 | this._super(event);
14 | this.get('dropdown').toggleDropdown(this.get('popoverName'), this);
15 | }
16 | });
17 |
18 | export default PopoverButton;
19 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-popover.js:
--------------------------------------------------------------------------------
1 | import GhostDropdown from 'ghost/components/gh-dropdown';
2 |
3 | var GhostPopover = GhostDropdown.extend({
4 | classNames: 'ghost-popover'
5 | });
6 |
7 | export default GhostPopover;
8 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-role-selector.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import GhostSelect from 'ghost/components/gh-select';
3 |
4 | var RolesSelector = GhostSelect.extend({
5 | roles: Ember.computed.alias('options'),
6 |
7 | options: Ember.computed(function () {
8 | var rolesPromise = this.store.find('role', {permissions: 'assign'});
9 |
10 | //汉化修改后台角色呈现名称
11 | this.store.find('role', { permissions: 'assign' }).then(function(item){
12 | item.forEach( function( i ){
13 | var n = i.get('description');
14 | i.set('name', n);
15 | });
16 | });
17 | var rolesPromise = this.store.find('role', { permissions: 'assign' });
18 |
19 |
20 | return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin)
21 | .create({promise: rolesPromise});
22 | })
23 | });
24 |
25 | export default RolesSelector;
26 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-tab-pane.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | // See gh-tabs-manager.js for use
3 | var TabPane = Ember.Component.extend({
4 | classNameBindings: ['active'],
5 |
6 | tabsManager: Ember.computed(function () {
7 | return this.nearestWithProperty('isTabsManager');
8 | }),
9 |
10 | tab: Ember.computed('tabsManager.tabs.[]', 'tabsManager.tabPanes.[]', function () {
11 | var index = this.get('tabsManager.tabPanes').indexOf(this),
12 | tabs = this.get('tabsManager.tabs');
13 |
14 | return tabs && tabs.objectAt(index);
15 | }),
16 |
17 | active: Ember.computed.alias('tab.active'),
18 |
19 | // Register with the tabs manager
20 | registerWithTabs: function () {
21 | this.get('tabsManager').registerTabPane(this);
22 | }.on('didInsertElement'),
23 |
24 | unregisterWithTabs: function () {
25 | this.get('tabsManager').unregisterTabPane(this);
26 | }.on('willDestroyElement')
27 | });
28 |
29 | export default TabPane;
30 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-tab.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | // See gh-tabs-manager.js for use
3 | var Tab = Ember.Component.extend({
4 | tabsManager: Ember.computed(function () {
5 | return this.nearestWithProperty('isTabsManager');
6 | }),
7 |
8 | active: Ember.computed('tabsManager.activeTab', function () {
9 | return this.get('tabsManager.activeTab') === this;
10 | }),
11 |
12 | index: Ember.computed('tabsManager.tabs.@each', function () {
13 | return this.get('tabsManager.tabs').indexOf(this);
14 | }),
15 |
16 | // Select on click
17 | click: function () {
18 | this.get('tabsManager').select(this);
19 | },
20 |
21 | // Registration methods
22 | registerWithTabs: function () {
23 | this.get('tabsManager').registerTab(this);
24 | }.on('didInsertElement'),
25 |
26 | unregisterWithTabs: function () {
27 | this.get('tabsManager').unregisterTab(this);
28 | }.on('willDestroyElement')
29 | });
30 |
31 | export default Tab;
32 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-textarea.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import TextInputMixin from 'ghost/mixins/text-input';
3 |
4 | var TextArea = Ember.TextArea.extend(TextInputMixin);
5 |
6 | export default TextArea;
7 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-trim-focus-input.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | /*global device*/
3 | var TrimFocusInput = Ember.TextField.extend({
4 | focus: true,
5 |
6 | attributeBindings: ['autofocus'],
7 |
8 | autofocus: Ember.computed(function () {
9 | if (this.get('focus')) {
10 | return (device.ios()) ? false : 'autofocus';
11 | }
12 |
13 | return false;
14 | }),
15 |
16 | didInsertElement: function () {
17 | // This fix is required until Mobile Safari has reliable
18 | // autofocus, select() or focus() support
19 | if (this.get('focus') && !device.ios()) {
20 | this.$().val(this.$().val()).focus();
21 | }
22 | },
23 |
24 | focusOut: function () {
25 | var text = this.$().val();
26 |
27 | this.$().val(text.trim());
28 | }
29 | });
30 |
31 | export default TrimFocusInput;
32 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-uploader.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import uploader from 'ghost/assets/lib/uploader';
3 |
4 | var PostImageUploader = Ember.Component.extend({
5 | classNames: ['image-uploader', 'js-post-image-upload'],
6 |
7 | imageSource: Ember.computed('image', function () {
8 | return this.get('image') || '';
9 | }),
10 |
11 | setup: function () {
12 | var $this = this.$(),
13 | self = this;
14 |
15 | this.set('uploaderReference', uploader.call($this, {
16 | editor: true,
17 | fileStorage: this.get('config.fileStorage')
18 | }));
19 |
20 | $this.on('uploadsuccess', function (event, result) {
21 | if (result && result !== '' && result !== 'http://') {
22 | self.sendAction('uploaded', result);
23 | }
24 | });
25 |
26 | $this.on('imagecleared', function () {
27 | self.sendAction('canceled');
28 | });
29 | }.on('didInsertElement'),
30 |
31 | removeListeners: function () {
32 | var $this = this.$();
33 |
34 | $this.off();
35 | $this.find('.js-cancel').off();
36 | }.on('willDestroyElement')
37 | });
38 |
39 | export default PostImageUploader;
40 |
--------------------------------------------------------------------------------
/core/client/app/components/gh-url-preview.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | /*
3 | Example usage:
4 | {{gh-url-preview prefix="tag" slug=theSlugValue tagName="p" classNames="description"}}
5 | */
6 | var urlPreview = Ember.Component.extend({
7 | classNames: 'ghost-url-preview',
8 | prefix: null,
9 | slug: null,
10 |
11 | url: Ember.computed('slug', function () {
12 | // Get the blog URL and strip the scheme
13 | var blogUrl = this.get('config').blogUrl,
14 | noSchemeBlogUrl = blogUrl.substr(blogUrl.indexOf('://') + 3), // Remove `http[s]://`
15 |
16 | // Get the prefix and slug values
17 | prefix = this.get('prefix') ? this.get('prefix') + '/' : '',
18 | slug = this.get('slug') ? this.get('slug') + '/' : '',
19 |
20 | // Join parts of the URL together with slashes
21 | theUrl = noSchemeBlogUrl + '/' + prefix + slug;
22 |
23 | return theUrl;
24 | })
25 | });
26 |
27 | export default urlPreview;
28 |
--------------------------------------------------------------------------------
/core/client/app/controllers/application.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var ApplicationController = Ember.Controller.extend({
3 | // jscs: disable
4 | hideNav: Ember.computed.match('currentPath', /(error|signin|signup|setup|forgotten|reset)/),
5 | // jscs: enable
6 |
7 | topNotificationCount: 0,
8 | showGlobalMobileNav: false,
9 | showSettingsMenu: false,
10 |
11 | userImage: Ember.computed('session.user.image', function () {
12 | return this.get('session.user.image') || this.get('ghostPaths.url').asset('/shared/img/user-image.png');
13 | }),
14 |
15 | userImageBackground: Ember.computed('userImage', function () {
16 | return `background-image: url(${this.get('userImage')})`.htmlSafe();
17 | }),
18 |
19 | userImageAlt: Ember.computed('session.user.name', function () {
20 | var name = this.get('session.user.name');
21 |
22 | return (name) ? name + '\'s profile picture' : 'Profile picture';
23 | }),
24 |
25 | actions: {
26 | topNotificationChange: function (count) {
27 | this.set('topNotificationCount', count);
28 | }
29 | }
30 | });
31 |
32 | export default ApplicationController;
33 |
--------------------------------------------------------------------------------
/core/client/app/controllers/editor/edit.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
3 |
4 | var EditorEditController = Ember.Controller.extend(EditorControllerMixin);
5 |
6 | export default EditorEditController;
7 |
--------------------------------------------------------------------------------
/core/client/app/controllers/editor/new.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
3 |
4 | var EditorNewController = Ember.Controller.extend(EditorControllerMixin, {
5 | // Overriding autoSave on the base controller, as the new controller shouldn't be autosaving
6 | autoSave: Ember.K,
7 | actions: {
8 | /**
9 | * Redirect to editor after the first save
10 | */
11 | save: function (options) {
12 | var self = this;
13 | return this._super(options).then(function (model) {
14 | if (model.get('id')) {
15 | self.replaceRoute('editor.edit', model);
16 | }
17 | });
18 | }
19 | }
20 | });
21 |
22 | export default EditorNewController;
23 |
--------------------------------------------------------------------------------
/core/client/app/controllers/error.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var ErrorController = Ember.Controller.extend({
3 | code: Ember.computed('content.status', function () {
4 | return this.get('content.status') > 200 ? this.get('content.status') : 500;
5 | }),
6 | message: Ember.computed('content.statusText', function () {
7 | if (this.get('code') === 404) {
8 | return 'No Ghost Found';
9 | }
10 |
11 | return this.get('content.statusText') !== 'error' ? this.get('content.statusText') : 'Internal Server Error';
12 | }),
13 | stack: false
14 | });
15 |
16 | export default ErrorController;
17 |
--------------------------------------------------------------------------------
/core/client/app/controllers/feature.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var FeatureController = Ember.Controller.extend(Ember.PromiseProxyMixin, {
3 | init: function () {
4 | var promise;
5 |
6 | promise = this.store.find('setting', {type: 'blog,theme'}).then(function (settings) {
7 | return settings.get('firstObject');
8 | });
9 |
10 | this.set('promise', promise);
11 | },
12 |
13 | setting: Ember.computed.alias('content'),
14 |
15 | labs: Ember.computed('isSettled', 'setting.labs', function () {
16 | var value = {};
17 |
18 | if (this.get('isFulfilled')) {
19 | try {
20 | value = JSON.parse(this.get('setting.labs') || {});
21 | } catch (err) {
22 | value = {};
23 | }
24 | }
25 |
26 | return value;
27 | })
28 | });
29 |
30 | export default FeatureController;
31 |
--------------------------------------------------------------------------------
/core/client/app/controllers/modals/copy-html.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var CopyHTMLController = Ember.Controller.extend({
3 |
4 | generatedHTML: Ember.computed.alias('model.generatedHTML')
5 |
6 | });
7 |
8 | export default CopyHTMLController;
9 |
--------------------------------------------------------------------------------
/core/client/app/controllers/modals/delete-all.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var DeleteAllController = Ember.Controller.extend({
3 | actions: {
4 | confirmAccept: function () {
5 | var self = this;
6 |
7 | ic.ajax.request(this.get('ghostPaths.url').api('db'), {
8 | type: 'DELETE'
9 | }).then(function () {
10 | self.notifications.showSuccess('删除成功');
11 | self.store.unloadAll('post');
12 | self.store.unloadAll('tag');
13 | }).catch(function (response) {
14 | self.notifications.showErrors(response);
15 | });
16 | },
17 |
18 | confirmReject: function () {
19 | return false;
20 | }
21 | },
22 |
23 | confirm: {
24 | accept: {
25 | text: '确认删除',
26 | buttonClass: 'btn btn-red'
27 | },
28 | reject: {
29 | text: '取消操作',
30 | buttonClass: 'btn btn-default btn-minor'
31 | }
32 | }
33 | });
34 |
35 | export default DeleteAllController;
36 |
--------------------------------------------------------------------------------
/core/client/app/controllers/modals/delete-post.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var DeletePostController = Ember.Controller.extend({
3 | actions: {
4 | confirmAccept: function () {
5 | var self = this,
6 | model = this.get('model');
7 |
8 | // definitely want to clear the data store and post of any unsaved, client-generated tags
9 | model.updateTags();
10 |
11 | model.destroyRecord().then(function () {
12 | self.get('dropdown').closeDropdowns();
13 | self.transitionToRoute('posts.index');
14 | self.notifications.showSuccess('删除成功', {delayed: true});
15 | }, function () {
16 | self.notifications.showError('删除失败,请重试');
17 | });
18 | },
19 |
20 | confirmReject: function () {
21 | return false;
22 | }
23 | },
24 |
25 | confirm: {
26 | accept: {
27 | text: '确认删除',
28 | buttonClass: 'btn btn-red'
29 | },
30 | reject: {
31 | text: '取消操作',
32 | buttonClass: 'btn btn-default btn-minor'
33 | }
34 | }
35 | });
36 |
37 | export default DeletePostController;
38 |
--------------------------------------------------------------------------------
/core/client/app/controllers/modals/delete-tag.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var DeleteTagController = Ember.Controller.extend({
3 | postInflection: Ember.computed('model.post_count', function () {
4 | return this.get('model.post_count') > 1 ? '博文' : '博文';
5 | }),
6 |
7 | actions: {
8 | confirmAccept: function () {
9 | var tag = this.get('model'),
10 | name = tag.get('name'),
11 | self = this;
12 |
13 | this.send('closeSettingsMenu');
14 |
15 | tag.destroyRecord().then(function () {
16 | self.notifications.showSuccess('删除 ' + name);
17 | }).catch(function (error) {
18 | self.notifications.showAPIError(error);
19 | });
20 | },
21 |
22 | confirmReject: function () {
23 | return false;
24 | }
25 | },
26 |
27 | confirm: {
28 | accept: {
29 | text: '确认删除',
30 | buttonClass: 'btn btn-red'
31 | },
32 | reject: {
33 | text: '取消操作',
34 | buttonClass: 'btn btn-default btn-minor'
35 | }
36 | }
37 | });
38 |
39 | export default DeleteTagController;
40 |
--------------------------------------------------------------------------------
/core/client/app/controllers/modals/upload.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | var UploadController = Ember.Controller.extend({
4 | acceptEncoding: 'image/*',
5 | actions: {
6 | confirmAccept: function () {
7 | var self = this;
8 |
9 | this.get('model').save().then(function (model) {
10 | self.notifications.showSuccess('保存成功');
11 | return model;
12 | }).catch(function (err) {
13 | self.notifications.showErrors(err);
14 | });
15 | },
16 |
17 | confirmReject: function () {
18 | return false;
19 | }
20 | }
21 | });
22 |
23 | export default UploadController;
24 |
--------------------------------------------------------------------------------
/core/client/app/controllers/posts/post.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var PostController = Ember.Controller.extend({
3 | isPublished: Ember.computed.equal('model.status', 'published'),
4 | classNameBindings: ['model.featured'],
5 |
6 | authorName: Ember.computed('model.author.name', 'model.author.email', function () {
7 | return this.get('model.author.name') || this.get('model.author.email');
8 | }),
9 |
10 | authorAvatar: Ember.computed('model.author.image', function () {
11 | return this.get('model.author.image') || this.get('ghostPaths.url').asset('/shared/img/user-image.png');
12 | }),
13 |
14 | authorAvatarBackground: Ember.computed('authorAvatar', function () {
15 | return `background-image: url(${this.get('authorAvatar')})`.htmlSafe();
16 | }),
17 |
18 | actions: {
19 | toggleFeatured: function () {
20 | var options = {disableNProgress: true},
21 | self = this;
22 |
23 | this.toggleProperty('model.featured');
24 | this.get('model').save(options).catch(function (errors) {
25 | self.notifications.showErrors(errors);
26 | });
27 | },
28 | showPostContent: function () {
29 | this.transitionToRoute('posts.post', this.get('model'));
30 | }
31 | }
32 | });
33 |
34 | export default PostController;
35 |
--------------------------------------------------------------------------------
/core/client/app/controllers/settings/about.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var SettingsAboutController = Ember.Controller.extend({
3 | updateNotificationCount: 0,
4 |
5 | actions: {
6 | updateNotificationChange: function (count) {
7 | this.set('updateNotificationCount', count);
8 | }
9 | }
10 | });
11 |
12 | export default SettingsAboutController;
13 |
--------------------------------------------------------------------------------
/core/client/app/controllers/settings/code-injection.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var SettingsCodeInjectionController = Ember.Controller.extend({
3 | actions: {
4 | save: function () {
5 | var self = this;
6 |
7 | return this.get('model').save().then(function (model) {
8 | self.notifications.closePassive();
9 | self.notifications.showSuccess('配置保存成功。');
10 |
11 | return model;
12 | }).catch(function (errors) {
13 | self.notifications.closePassive();
14 | self.notifications.showErrors(errors);
15 | });
16 | }
17 | }
18 | });
19 |
20 | export default SettingsCodeInjectionController;
21 |
--------------------------------------------------------------------------------
/core/client/app/controllers/settings/users/index.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import PaginationControllerMixin from 'ghost/mixins/pagination-controller';
3 |
4 | var UsersIndexController = Ember.ArrayController.extend(PaginationControllerMixin, {
5 | init: function () {
6 | // let the PaginationControllerMixin know what type of model we will be paginating
7 | // this is necessary because we do not have access to the model inside the Controller::init method
8 | this._super({modelType: 'user'});
9 | },
10 |
11 | users: Ember.computed.alias('model'),
12 |
13 | activeUsers: Ember.computed.filter('users', function (user) {
14 | return /^active|warn-[1-4]|locked$/.test(user.get('status'));
15 | }),
16 |
17 | invitedUsers: Ember.computed.filter('users', function (user) {
18 | var status = user.get('status');
19 |
20 | return status === 'invited' || status === 'invited-pending';
21 | })
22 | });
23 |
24 | export default UsersIndexController;
25 |
--------------------------------------------------------------------------------
/core/client/app/helpers/gh-count-characters.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var countCharacters = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
3 | var el = document.createElement('span'),
4 | length,
5 | content;
6 |
7 | if (!arr || !arr.length) {
8 | return;
9 | }
10 |
11 | content = arr[0] || '';
12 | length = content.length;
13 |
14 | el.className = 'word-count';
15 |
16 | if (length > 180) {
17 | el.style.color = '#E25440';
18 | } else {
19 | el.style.color = '#9E9D95';
20 | }
21 |
22 | el.innerHTML = 200 - length;
23 |
24 | return Ember.String.htmlSafe(el.outerHTML);
25 | });
26 |
27 | export default countCharacters;
28 |
--------------------------------------------------------------------------------
/core/client/app/helpers/gh-count-down-characters.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | var countDownCharacters = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
3 | var el = document.createElement('span'),
4 | content,
5 | maxCharacters,
6 | length;
7 |
8 | if (!arr || arr.length < 2) {
9 | return;
10 | }
11 |
12 | content = arr[0] || '';
13 | maxCharacters = arr[1];
14 | length = content.length;
15 |
16 | el.className = 'word-count';
17 |
18 | if (length > maxCharacters) {
19 | el.style.color = '#E25440';
20 | } else {
21 | el.style.color = '#9FBB58';
22 | }
23 |
24 | el.innerHTML = length;
25 |
26 | return Ember.String.htmlSafe(el.outerHTML);
27 | });
28 |
29 | export default countDownCharacters;
30 |
--------------------------------------------------------------------------------
/core/client/app/helpers/gh-count-words.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import counter from 'ghost/utils/word-count';
3 |
4 | var countWords = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
5 | if (!arr || !arr.length) {
6 | return;
7 | }
8 |
9 | var markdown,
10 | count;
11 |
12 | markdown = arr[0] || '';
13 |
14 | if (/^\s*$/.test(markdown)) {
15 | return '0 个字';
16 | }
17 |
18 | count = counter(markdown);
19 |
20 | return count + (count === 1 ? ' 个字' : ' 个字');
21 |
22 | });
23 |
24 | export default countWords;
25 |
--------------------------------------------------------------------------------
/core/client/app/helpers/gh-format-html.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | /* global html_sanitize*/
3 | import cajaSanitizers from 'ghost/utils/caja-sanitizers';
4 |
5 | var formatHTML = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
6 | if (!arr || !arr.length) {
7 | return;
8 | }
9 |
10 | var escapedhtml = arr[0] || '';
11 |
12 | // replace script and iFrame
13 | escapedhtml = escapedhtml.replace(/
25 |
26 |
27 |
28 |
29 |
30 | {{content-for 'body-footer'}}
31 | {{content-for 'test-body-footer'}}
32 |