├── .gitignore ├── Gruntfile.js ├── LICENSE ├── PRIVACY.md ├── README.md ├── azuredeploy.json ├── bower.json ├── config.example.js ├── config.js ├── content ├── apps │ └── README.md ├── data │ └── README.md ├── images │ └── README.md └── themes │ └── casper │ ├── LICENSE │ ├── README.md │ ├── assets │ ├── css │ │ └── screen.css │ ├── fonts │ │ ├── casper-icons.eot │ │ ├── casper-icons.svg │ │ ├── casper-icons.ttf │ │ └── casper-icons.woff │ └── js │ │ ├── index.js │ │ └── jquery.fitvids.js │ ├── author.hbs │ ├── default.hbs │ ├── index.hbs │ ├── package.json │ ├── page.hbs │ ├── partials │ ├── loop.hbs │ └── navigation.hbs │ ├── post.hbs │ └── tag.hbs ├── core ├── built │ └── assets │ │ ├── codemirror │ │ ├── codemirror.css │ │ └── codemirror.js │ │ ├── fonts │ │ ├── ghosticons.eot │ │ ├── ghosticons.svg │ │ ├── ghosticons.ttf │ │ └── ghosticons.woff │ │ ├── ghost.css │ │ ├── ghost.js │ │ ├── ghost.min.css │ │ ├── ghost.min.js │ │ ├── img │ │ ├── 404-ghost.png │ │ ├── 404-ghost@2x.png │ │ ├── contributors │ │ │ ├── AileenCGN │ │ │ ├── ErisDS │ │ │ ├── acburdine │ │ │ ├── cobbspur │ │ │ ├── dbalders │ │ │ ├── eexit │ │ │ ├── felixrieseberg │ │ │ ├── gergelyke │ │ │ ├── imbrian │ │ │ ├── jaswilli │ │ │ ├── juanpaco │ │ │ ├── kevinansfield │ │ │ ├── kirrg001 │ │ │ ├── sakulstra │ │ │ ├── sebgie │ │ │ ├── starcwl │ │ │ ├── vkandy │ │ │ └── zhenkyle │ │ ├── ghost-logo.png │ │ ├── ghosticon.jpg │ │ ├── install-welcome.png │ │ ├── invite-placeholder.png │ │ ├── large.png │ │ ├── loadingcat.gif │ │ ├── medium.png │ │ ├── slackicon.png │ │ ├── small.png │ │ ├── touch-icon-ipad.png │ │ ├── touch-icon-iphone.png │ │ ├── user-cover.png │ │ ├── user-image.png │ │ └── users.png │ │ ├── tests.js │ │ ├── vendor.css │ │ ├── vendor.js │ │ ├── vendor.min.css │ │ └── vendor.min.js ├── index.js ├── server │ ├── api │ │ ├── authentication.js │ │ ├── clients.js │ │ ├── configuration.js │ │ ├── db.js │ │ ├── index.js │ │ ├── mail.js │ │ ├── notifications.js │ │ ├── posts.js │ │ ├── roles.js │ │ ├── schedules.js │ │ ├── settings.js │ │ ├── slack.js │ │ ├── slugs.js │ │ ├── subscribers.js │ │ ├── tags.js │ │ ├── themes.js │ │ ├── upload.js │ │ ├── users.js │ │ └── utils.js │ ├── apps │ │ ├── dependencies.js │ │ ├── index.js │ │ ├── loader.js │ │ ├── permissions.js │ │ ├── private-blogging │ │ │ ├── index.js │ │ │ ├── lib │ │ │ │ ├── middleware.js │ │ │ │ ├── router.js │ │ │ │ └── views │ │ │ │ │ └── private.hbs │ │ │ ├── robots.txt │ │ │ └── tests │ │ │ │ ├── controller_spec.js │ │ │ │ └── middleware_spec.js │ │ ├── proxy.js │ │ ├── sandbox.js │ │ └── subscribers │ │ │ ├── index.js │ │ │ └── lib │ │ │ ├── router.js │ │ │ └── views │ │ │ └── subscribe.hbs │ ├── config │ │ ├── index.js │ │ └── url.js │ ├── controllers │ │ ├── admin.js │ │ └── frontend │ │ │ ├── channel-config.js │ │ │ ├── channels.js │ │ │ ├── context.js │ │ │ ├── error.js │ │ │ ├── fetch-data.js │ │ │ ├── format-response.js │ │ │ ├── index.js │ │ │ ├── post-lookup.js │ │ │ ├── render-channel.js │ │ │ ├── secure.js │ │ │ └── templates.js │ ├── data │ │ ├── db │ │ │ ├── connection.js │ │ │ └── index.js │ │ ├── export │ │ │ └── index.js │ │ ├── import │ │ │ ├── data-importer.js │ │ │ ├── index.js │ │ │ └── utils.js │ │ ├── importer │ │ │ ├── handlers │ │ │ │ ├── image.js │ │ │ │ ├── json.js │ │ │ │ └── markdown.js │ │ │ ├── importers │ │ │ │ ├── data.js │ │ │ │ └── image.js │ │ │ └── index.js │ │ ├── meta │ │ │ ├── asset_url.js │ │ │ ├── author_fb_url.js │ │ │ ├── author_image.js │ │ │ ├── author_url.js │ │ │ ├── canonical_url.js │ │ │ ├── context_object.js │ │ │ ├── cover_image.js │ │ │ ├── creator_url.js │ │ │ ├── description.js │ │ │ ├── excerpt.js │ │ │ ├── index.js │ │ │ ├── keywords.js │ │ │ ├── modified_date.js │ │ │ ├── og_type.js │ │ │ ├── paginated_url.js │ │ │ ├── published_date.js │ │ │ ├── rss_url.js │ │ │ ├── schema.js │ │ │ ├── structured_data.js │ │ │ ├── title.js │ │ │ └── url.js │ │ ├── migration │ │ │ ├── 004 │ │ │ │ ├── 01-add-tour-column-to-users.js │ │ │ │ ├── 02-add-sortorder-column-to-poststags.js │ │ │ │ ├── 03-add-many-columns-to-clients.js │ │ │ │ ├── 04-add-clienttrusteddomains-table.js │ │ │ │ ├── 05-drop-unique-on-clients-secret.js │ │ │ │ └── index.js │ │ │ ├── 005 │ │ │ │ ├── 01-drop-hidden-column-from-tags.js │ │ │ │ ├── 02-add-visibility-column-to-key-tables.js │ │ │ │ ├── 03-add-mobiledoc-column-to-posts.js │ │ │ │ ├── 04-add-social-media-columns-to-users.js │ │ │ │ ├── 05-add-subscribers-table.js │ │ │ │ └── index.js │ │ │ ├── 006 │ │ │ │ └── index.js │ │ │ ├── backup.js │ │ │ ├── fixtures │ │ │ │ ├── 004 │ │ │ │ │ ├── 01-move-jquery-with-alert.js │ │ │ │ │ ├── 02-update-private-setting-type.js │ │ │ │ │ ├── 03-update-password-setting-type.js │ │ │ │ │ ├── 04-update-ghost-admin-client.js │ │ │ │ │ ├── 05-add-ghost-frontend-client.js │ │ │ │ │ ├── 06-clean-broken-tags.js │ │ │ │ │ ├── 07-add-post-tag-order.js │ │ │ │ │ ├── 08-add-post-fixture.js │ │ │ │ │ └── index.js │ │ │ │ ├── 005 │ │ │ │ │ ├── 01-update-ghost-client-secrets.js │ │ │ │ │ ├── 02-add-ghost-scheduler-client.js │ │ │ │ │ ├── 03-add-client-permissions.js │ │ │ │ │ ├── 04-add-subscriber-permissions.js │ │ │ │ │ └── index.js │ │ │ │ ├── 006 │ │ │ │ │ ├── 01-transform-dates-into-utc.js │ │ │ │ │ └── index.js │ │ │ │ ├── fixtures.json │ │ │ │ ├── index.js │ │ │ │ ├── populate.js │ │ │ │ ├── update.js │ │ │ │ └── utils.js │ │ │ ├── index.js │ │ │ ├── populate.js │ │ │ ├── reset.js │ │ │ └── update.js │ │ ├── schema │ │ │ ├── checks.js │ │ │ ├── clients │ │ │ │ ├── index.js │ │ │ │ ├── mysql.js │ │ │ │ ├── pg.js │ │ │ │ └── sqlite3.js │ │ │ ├── commands.js │ │ │ ├── default-settings.json │ │ │ ├── index.js │ │ │ ├── schema.js │ │ │ └── versioning.js │ │ ├── slack │ │ │ └── index.js │ │ ├── timezones.json │ │ ├── validation │ │ │ └── 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 │ ├── errors │ │ ├── bad-request-error.js │ │ ├── data-import-error.js │ │ ├── database-not-populated.js │ │ ├── database-version.js │ │ ├── email-error.js │ │ ├── incorrect-usage.js │ │ ├── index.js │ │ ├── internal-server-error.js │ │ ├── maintenance.js │ │ ├── method-not-allowed-error.js │ │ ├── no-permission-error.js │ │ ├── not-found-error.js │ │ ├── request-too-large-error.js │ │ ├── token-revocation-error.js │ │ ├── too-many-requests-error.js │ │ ├── unauthorized-error.js │ │ ├── unsupported-media-type-error.js │ │ ├── validation-error.js │ │ └── version-mismatch-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 │ │ ├── facebook_url.js │ │ ├── foreach.js │ │ ├── get.js │ │ ├── ghost_foot.js │ │ ├── ghost_head.js │ │ ├── has.js │ │ ├── image.js │ │ ├── index.js │ │ ├── input_email.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 │ │ │ └── subscribe_form.hbs │ │ ├── twitter_url.js │ │ ├── url.js │ │ └── utils.js │ ├── i18n.js │ ├── index.js │ ├── mail │ │ ├── GhostMailer.js │ │ ├── index.js │ │ ├── templates │ │ │ ├── invite-user.html │ │ │ ├── newsletter.html │ │ │ ├── raw │ │ │ │ ├── invite-user.html │ │ │ │ ├── reset-password.html │ │ │ │ ├── test.html │ │ │ │ └── welcome.html │ │ │ ├── reset-password.html │ │ │ ├── test.html │ │ │ └── welcome.html │ │ └── utils.js │ ├── middleware │ │ ├── api │ │ │ └── version-match.js │ │ ├── auth-strategies.js │ │ ├── auth.js │ │ ├── cache-control.js │ │ ├── check-ssl.js │ │ ├── cors.js │ │ ├── decide-is-admin.js │ │ ├── index.js │ │ ├── labs.js │ │ ├── maintenance.js │ │ ├── oauth.js │ │ ├── redirect-to-setup.js │ │ ├── serve-shared-file.js │ │ ├── spam-prevention.js │ │ ├── static-theme.js │ │ ├── theme-handler.js │ │ └── uncapitalise.js │ ├── models │ │ ├── accesstoken.js │ │ ├── app-field.js │ │ ├── app-setting.js │ │ ├── app.js │ │ ├── base │ │ │ ├── index.js │ │ │ ├── listeners.js │ │ │ ├── token.js │ │ │ └── utils.js │ │ ├── client-trusted-domain.js │ │ ├── client.js │ │ ├── index.js │ │ ├── permission.js │ │ ├── plugins │ │ │ ├── access-rules.js │ │ │ ├── filter.js │ │ │ ├── include-count.js │ │ │ ├── index.js │ │ │ └── pagination.js │ │ ├── post.js │ │ ├── refreshtoken.js │ │ ├── role.js │ │ ├── settings.js │ │ ├── subscriber.js │ │ ├── tag.js │ │ └── user.js │ ├── overrides.js │ ├── permissions │ │ ├── effective.js │ │ └── index.js │ ├── routes │ │ ├── admin.js │ │ ├── api.js │ │ ├── frontend.js │ │ └── index.js │ ├── scheduling │ │ ├── SchedulingBase.js │ │ ├── SchedulingDefault.js │ │ ├── index.js │ │ ├── post-scheduling │ │ │ └── index.js │ │ └── utils.js │ ├── storage │ │ ├── base.js │ │ ├── index.js │ │ └── local-file-store.js │ ├── translations │ │ └── en.json │ ├── update-check.js │ ├── utils │ │ ├── downzero.js │ │ ├── gravatar.js │ │ ├── index.js │ │ ├── labs.js │ │ ├── npm │ │ │ └── preinstall.js │ │ ├── parse-package-json.js │ │ ├── pipeline.js │ │ ├── read-csv.js │ │ ├── read-directory.js │ │ ├── read-themes.js │ │ ├── sequence.js │ │ ├── social-urls.js │ │ ├── startup-check.js │ │ └── validate-themes.js │ └── views │ │ ├── default.hbs │ │ └── user-error.hbs └── shared │ ├── favicon.ico │ ├── ghost-url.js │ ├── ghost-url.min.js │ ├── robots.txt │ └── sitemap.xsl ├── docs └── update.png ├── iisnode.yml ├── index.js ├── npm-shrinkwrap.json ├── package.json └── web.config /.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 | .bowerrc 18 | .idea/* 19 | *.iml 20 | *.sublime-* 21 | projectFilesBackup 22 | 23 | .DS_Store 24 | 25 | # vim-related 26 | [._]*.s[a-w][a-z] 27 | [._]s[a-w][a-z] 28 | *.un~ 29 | Session.vim 30 | .netrwhist 31 | .vimrc 32 | *~ 33 | 34 | # TernJS 35 | .tern-project 36 | 37 | # Ghost DB file 38 | *.db 39 | *.db-journal 40 | 41 | .build 42 | .dist 43 | .tmp 44 | 45 | # Changelog, which is autogenerated, not committed 46 | CHANGELOG.md 47 | 48 | # Casper generated files 49 | /core/test/functional/*.png 50 | 51 | # Coverage reports 52 | coverage.html 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016 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 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghost", 3 | "dependencies": { 4 | "codemirror": "4.0.1", 5 | "Countable": "2.0.2", 6 | "device": "git://github.com/matthewhudson/device.js#5347a275b66020a0d4dfe9aad81a488f8cce448d", 7 | "ember": "1.10.0", 8 | "ember-data": "1.0.0-beta.14.1", 9 | "ember-load-initializers": "git://github.com/stefanpenner/ember-load-initializers.git#0.0.1", 10 | "ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#181251821cf513bb58d3e192faa13245a816f75e", 11 | "ember-simple-auth": "0.7.2", 12 | "fastclick": "1.0.0", 13 | "handlebars": "2.0.0", 14 | "ic-ajax": "1.0.1", 15 | "jquery": "1.11.0", 16 | "jquery-file-upload": "9.5.6", 17 | "jquery-hammerjs": "1.0.1", 18 | "jquery-ui": "1.10.4", 19 | "jqueryui-touch-punch": "furf/jquery-ui-touch-punch", 20 | "keymaster": "git://github.com/madrobby/keymaster#564ea42e07de40da8113a571f17ceae8802672ff", 21 | "loader.js": "git://github.com/stefanpenner/loader.js#1.0.0", 22 | "moment": "2.8.3", 23 | "nanoscroller": "0.8.4", 24 | "normalize-scss": "~3.0.1", 25 | "nprogress": "0.1.2", 26 | "showdown-ghost": "0.3.4", 27 | "validator-js": "3.28.0", 28 | "google-caja": "5669.0.0" 29 | }, 30 | "devDependencies": { 31 | "ember-mocha": "~0.3.0", 32 | "ember-cli-test-loader": "dgeb/ember-cli-test-loader#test-agnostic", 33 | "ember-cli-shims": "stefanpenner/ember-cli-shims#~0.0.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | This is the home of your Ghost database, do not overwrite this folder or any of the files inside of it. -------------------------------------------------------------------------------- /content/images/README.md: -------------------------------------------------------------------------------- 1 | # Content / Images 2 | 3 | If using the standard file storage, Ghost will upload images to this directory. -------------------------------------------------------------------------------- /content/themes/casper/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016 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 | -------------------------------------------------------------------------------- /content/themes/casper/README.md: -------------------------------------------------------------------------------- 1 | # Casper 2 | 3 | The default theme for [Ghost](http://github.com/tryghost/ghost/). 4 | 5 | To download, visit the [releases](https://github.com/TryGhost/Casper/releases) page. 6 | 7 | ## Copyright & License 8 | 9 | Copyright (c) 2013-2016 Ghost Foundation - Released under the MIT License. 10 | 11 | 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: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | 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 16 | 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. 17 | -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/content/themes/casper/assets/fonts/casper-icons.eot -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/content/themes/casper/assets/fonts/casper-icons.ttf -------------------------------------------------------------------------------- /content/themes/casper/assets/fonts/casper-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/content/themes/casper/assets/fonts/casper-icons.woff -------------------------------------------------------------------------------- /content/themes/casper/assets/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main JS file for Casper behaviours 3 | */ 4 | 5 | /* globals jQuery, document */ 6 | (function ($, undefined) { 7 | "use strict"; 8 | 9 | var $document = $(document); 10 | 11 | $document.ready(function () { 12 | 13 | var $postContent = $(".post-content"); 14 | $postContent.fitVids(); 15 | 16 | $(".scroll-down").arctic_scroll(); 17 | 18 | $(".menu-button, .nav-cover, .nav-close").on("click", function(e){ 19 | e.preventDefault(); 20 | $("body").toggleClass("nav-opened nav-closed"); 21 | }); 22 | 23 | }); 24 | 25 | // Arctic Scroll by Paul Adam Davis 26 | // https://github.com/PaulAdamDavis/Arctic-Scroll 27 | $.fn.arctic_scroll = function (options) { 28 | 29 | var defaults = { 30 | elem: $(this), 31 | speed: 500 32 | }, 33 | 34 | allOptions = $.extend(defaults, options); 35 | 36 | allOptions.elem.click(function (event) { 37 | event.preventDefault(); 38 | var $this = $(this), 39 | $htmlBody = $('html, body'), 40 | offset = ($this.attr('data-offset')) ? $this.attr('data-offset') : false, 41 | position = ($this.attr('data-position')) ? $this.attr('data-position') : false, 42 | toMove; 43 | 44 | if (offset) { 45 | toMove = parseInt(offset); 46 | $htmlBody.stop(true, false).animate({scrollTop: ($(this.hash).offset().top + toMove) }, allOptions.speed); 47 | } else if (position) { 48 | toMove = parseInt(position); 49 | $htmlBody.stop(true, false).animate({scrollTop: toMove }, allOptions.speed); 50 | } else { 51 | $htmlBody.stop(true, false).animate({scrollTop: ($(this.hash).offset().top) }, allOptions.speed); 52 | } 53 | }); 54 | 55 | }; 56 | })(jQuery); 57 | -------------------------------------------------------------------------------- /content/themes/casper/author.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{!-- The tag above means - insert everything in this file into the {body} of the default.hbs template --}} 3 | 4 | {{!-- The big featured header --}} 5 | 6 | {{!-- Everything inside the #author tags pulls data from the author --}} 7 | {{#author}} 8 |
9 | 15 |
16 | 17 |
18 | {{#if image}} 19 |
20 |
21 |
22 | {{/if}} 23 |

{{name}}

24 | {{#if bio}} 25 |

{{bio}}

26 | {{/if}} 27 |
28 | {{#if location}}{{location}}{{/if}} 29 | {{#if website}}{{website}}{{/if}} 30 | {{plural ../pagination.total empty='No posts' singular='% post' plural='% posts'}} 31 |
32 |
33 | {{/author}} 34 | 35 | {{!-- The main content area on the homepage --}} 36 |
37 | 38 | {{!-- The tag below includes the post loop - partials/loop.hbs --}} 39 | {{> "loop"}} 40 | 41 |
42 | -------------------------------------------------------------------------------- /content/themes/casper/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{!-- Document Settings --}} 5 | 6 | 7 | 8 | {{!-- Page Meta --}} 9 | {{meta_title}} 10 | 11 | 12 | {{!-- Mobile Meta --}} 13 | 14 | 15 | 16 | {{!-- Brand icon --}} 17 | 18 | 19 | {{!-- Styles'n'Scripts --}} 20 | 21 | 22 | 23 | {{!-- Ghost outputs important style and meta data with this tag --}} 24 | {{ghost_head}} 25 | 26 | 27 | 28 | {{!-- The blog navigation links --}} 29 | {{navigation}} 30 | 31 |
32 | 33 | {{!-- All the main content gets inserted here, index.hbs, post.hbs, etc --}} 34 | {{{body}}} 35 | 36 | {{!-- The tiny footer at the very bottom --}} 37 | 41 | 42 |
43 | 44 | {{!-- jQuery needs to come before `{{ghost_foot}}` so that jQuery can be used in code injection --}} 45 | 46 | {{!-- Ghost outputs important scripts and data with this tag --}} 47 | {{ghost_foot}} 48 | {{!-- Fitvids makes video embeds responsive and awesome --}} 49 | 50 | {{!-- The main JavaScript file for Casper --}} 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /content/themes/casper/index.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{!-- The tag above means - insert everything in this file into the {body} of the default.hbs template --}} 3 | 4 | {{!-- The big featured header --}} 5 |
6 | 12 |
13 |
14 |

{{@blog.title}}

15 |

{{@blog.description}}

16 |
17 |
18 | 19 |
20 | 21 | {{!-- The main content area on the homepage --}} 22 |
23 | 24 | {{!-- The tag below includes the post loop - partials/loop.hbs --}} 25 | {{> "loop"}} 26 | 27 |
28 | -------------------------------------------------------------------------------- /content/themes/casper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Casper", 3 | "version": "1.3.1" 4 | } 5 | -------------------------------------------------------------------------------- /content/themes/casper/page.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | 3 | {{!-- This is a page template. A page outputs content just like any other post, and has all the same 4 | attributes by default, but you can also customise it to behave differently if you prefer. --}} 5 | 6 | {{!-- Everything inside the #post tags pulls data from the page --}} 7 | {{#post}} 8 | 9 |
10 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |

{{title}}

23 |
24 | 25 |
26 | {{content}} 27 |
28 | 29 |
30 |
31 | {{/post}} 32 | -------------------------------------------------------------------------------- /content/themes/casper/partials/loop.hbs: -------------------------------------------------------------------------------- 1 | {{!-- Previous/next page links - only displayed on page 2+ --}} 2 |
3 | {{pagination}} 4 |
5 | 6 | {{!-- This is the post loop - each post will be output using this markup --}} 7 | {{#foreach posts}} 8 |
9 |
10 |

{{title}}

11 |
12 |
13 |

{{excerpt words="26"}} »

14 |
15 | 21 |
22 | {{/foreach}} 23 | 24 | {{!-- Previous/next page links - displayed on every page --}} 25 | {{pagination}} 26 | -------------------------------------------------------------------------------- /content/themes/casper/partials/navigation.hbs: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /content/themes/casper/tag.hbs: -------------------------------------------------------------------------------- 1 | {{!< default}} 2 | {{!-- The tag above means - insert everything in this file into the {body} of the default.hbs template --}} 3 | 4 | {{!-- If we have a tag cover, display that - else blog cover - else nothing --}} 5 |
6 | 12 |
13 | {{#tag}} 14 |
15 |

{{name}}

16 |

17 | {{#if description}} 18 | {{description}} 19 | {{else}} 20 | A {{../pagination.total}}-post collection 21 | {{/if}} 22 |

23 |
24 | {{/tag}} 25 |
26 |
27 | 28 | {{!-- The main content area on the homepage --}} 29 |
30 | 31 | {{!-- The tag below includes the post loop - partials/loop.hbs --}} 32 | {{> "loop"}} 33 | 34 |
35 | -------------------------------------------------------------------------------- /core/built/assets/fonts/ghosticons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/fonts/ghosticons.eot -------------------------------------------------------------------------------- /core/built/assets/fonts/ghosticons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/fonts/ghosticons.ttf -------------------------------------------------------------------------------- /core/built/assets/fonts/ghosticons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/fonts/ghosticons.woff -------------------------------------------------------------------------------- /core/built/assets/img/404-ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/404-ghost.png -------------------------------------------------------------------------------- /core/built/assets/img/404-ghost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/404-ghost@2x.png -------------------------------------------------------------------------------- /core/built/assets/img/contributors/AileenCGN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/AileenCGN -------------------------------------------------------------------------------- /core/built/assets/img/contributors/ErisDS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/ErisDS -------------------------------------------------------------------------------- /core/built/assets/img/contributors/acburdine: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/acburdine -------------------------------------------------------------------------------- /core/built/assets/img/contributors/cobbspur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/cobbspur -------------------------------------------------------------------------------- /core/built/assets/img/contributors/dbalders: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/dbalders -------------------------------------------------------------------------------- /core/built/assets/img/contributors/eexit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/eexit -------------------------------------------------------------------------------- /core/built/assets/img/contributors/felixrieseberg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/felixrieseberg -------------------------------------------------------------------------------- /core/built/assets/img/contributors/gergelyke: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/gergelyke -------------------------------------------------------------------------------- /core/built/assets/img/contributors/imbrian: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/imbrian -------------------------------------------------------------------------------- /core/built/assets/img/contributors/jaswilli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/jaswilli -------------------------------------------------------------------------------- /core/built/assets/img/contributors/juanpaco: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/juanpaco -------------------------------------------------------------------------------- /core/built/assets/img/contributors/kevinansfield: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/kevinansfield -------------------------------------------------------------------------------- /core/built/assets/img/contributors/kirrg001: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/kirrg001 -------------------------------------------------------------------------------- /core/built/assets/img/contributors/sakulstra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/sakulstra -------------------------------------------------------------------------------- /core/built/assets/img/contributors/sebgie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/sebgie -------------------------------------------------------------------------------- /core/built/assets/img/contributors/starcwl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/starcwl -------------------------------------------------------------------------------- /core/built/assets/img/contributors/vkandy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/vkandy -------------------------------------------------------------------------------- /core/built/assets/img/contributors/zhenkyle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/contributors/zhenkyle -------------------------------------------------------------------------------- /core/built/assets/img/ghost-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/ghost-logo.png -------------------------------------------------------------------------------- /core/built/assets/img/ghosticon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/ghosticon.jpg -------------------------------------------------------------------------------- /core/built/assets/img/install-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/install-welcome.png -------------------------------------------------------------------------------- /core/built/assets/img/invite-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/invite-placeholder.png -------------------------------------------------------------------------------- /core/built/assets/img/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/large.png -------------------------------------------------------------------------------- /core/built/assets/img/loadingcat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/loadingcat.gif -------------------------------------------------------------------------------- /core/built/assets/img/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/medium.png -------------------------------------------------------------------------------- /core/built/assets/img/slackicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/slackicon.png -------------------------------------------------------------------------------- /core/built/assets/img/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/small.png -------------------------------------------------------------------------------- /core/built/assets/img/touch-icon-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/touch-icon-ipad.png -------------------------------------------------------------------------------- /core/built/assets/img/touch-icon-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/touch-icon-iphone.png -------------------------------------------------------------------------------- /core/built/assets/img/user-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/user-cover.png -------------------------------------------------------------------------------- /core/built/assets/img/user-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/user-image.png -------------------------------------------------------------------------------- /core/built/assets/img/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/core/built/assets/img/users.png -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | // ## Server Loader 2 | // Passes options through the boot process to get a server instance back 3 | var server = require('./server'); 4 | 5 | // Set the default environment to be `development` 6 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 7 | 8 | function makeGhost(options) { 9 | options = options || {}; 10 | 11 | return server(options); 12 | } 13 | 14 | module.exports = makeGhost; 15 | -------------------------------------------------------------------------------- /core/server/api/clients.js: -------------------------------------------------------------------------------- 1 | // # Client API 2 | // RESTful API for the Client resource 3 | var Promise = require('bluebird'), 4 | _ = require('lodash'), 5 | dataProvider = require('../models'), 6 | errors = require('../errors'), 7 | utils = require('./utils'), 8 | pipeline = require('../utils/pipeline'), 9 | i18n = require('../i18n'), 10 | 11 | docName = 'clients', 12 | clients; 13 | 14 | /** 15 | * ### Clients API Methods 16 | * 17 | * **See:** [API Methods](index.js.html#api%20methods) 18 | */ 19 | clients = { 20 | 21 | /** 22 | * ## Read 23 | * @param {{id}} options 24 | * @return {Promise} Client 25 | */ 26 | read: function read(options) { 27 | var attrs = ['id', 'slug'], 28 | tasks; 29 | 30 | /** 31 | * ### Model Query 32 | * Make the call to the Model layer 33 | * @param {Object} options 34 | * @returns {Object} options 35 | */ 36 | function doQuery(options) { 37 | // only User Agent (type = `ua`) clients are available at the moment. 38 | options.data = _.extend(options.data, {type: 'ua'}); 39 | return dataProvider.Client.findOne(options.data, _.omit(options, ['data'])); 40 | } 41 | 42 | // Push all of our tasks into a `tasks` array in the correct order 43 | tasks = [ 44 | utils.validate(docName, {attrs: attrs}), 45 | // TODO: add permissions 46 | // utils.handlePublicPermissions(docName, 'read'), 47 | doQuery 48 | ]; 49 | 50 | // Pipeline calls each task passing the result of one to be the arguments for the next 51 | return pipeline(tasks, options).then(function formatResponse(result) { 52 | if (result) { 53 | return {clients: [result.toJSON(options)]}; 54 | } 55 | 56 | return Promise.reject(new errors.NotFoundError(i18n.t('common.api.clients.clientNotFound'))); 57 | }); 58 | } 59 | }; 60 | 61 | module.exports = clients; 62 | -------------------------------------------------------------------------------- /core/server/api/slack.js: -------------------------------------------------------------------------------- 1 | // # Slack API 2 | // API for sending Test Notifications to Slack 3 | var events = require('../events'), 4 | Promise = require('bluebird'), 5 | slack; 6 | 7 | /** 8 | * ## Slack API Method 9 | * 10 | * **See:** [API Methods](index.js.html#api%20methods) 11 | * @typedef Slack 12 | * @param slack 13 | */ 14 | slack = { 15 | /** 16 | * ### SendTest 17 | * Send a test notification 18 | * 19 | * @public 20 | */ 21 | sendTest: function () { 22 | events.emit('slack.test'); 23 | return Promise.resolve(); 24 | } 25 | }; 26 | 27 | module.exports = slack; 28 | -------------------------------------------------------------------------------- /core/server/api/upload.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'), 2 | Promise = require('bluebird'), 3 | fs = require('fs-extra'), 4 | pUnlink = Promise.promisify(fs.unlink), 5 | storage = require('../storage'), 6 | errors = require('../errors'), 7 | utils = require('./utils'), 8 | i18n = require('../i18n'), 9 | 10 | upload; 11 | 12 | /** 13 | * ## Upload API Methods 14 | * 15 | * **See:** [API Methods](index.js.html#api%20methods) 16 | */ 17 | upload = { 18 | 19 | /** 20 | * ### Add Image 21 | * 22 | * @public 23 | * @param {{context}} options 24 | * @returns {Promise} location of uploaded file 25 | */ 26 | add: Promise.method(function (options) { 27 | var store = storage.getStorage(); 28 | 29 | // Public interface of the storage module's `save` method requires 30 | // the file's name to be on the .name property. 31 | options.name = options.originalname; 32 | options.type = options.mimetype; 33 | 34 | // Check if a file was provided 35 | if (!utils.checkFileExists(options)) { 36 | throw new errors.NoPermissionError(i18n.t('errors.api.upload.pleaseSelectImage')); 37 | } 38 | 39 | // Check if the file is valid 40 | if (!utils.checkFileIsValid(options, config.uploads.contentTypes, config.uploads.extensions)) { 41 | throw new errors.UnsupportedMediaTypeError(i18n.t('errors.api.upload.pleaseSelectValidImage')); 42 | } 43 | 44 | return store.save(options).finally(function () { 45 | // Remove uploaded file from tmp location 46 | return pUnlink(options.path); 47 | }); 48 | }) 49 | }; 50 | 51 | module.exports = upload; 52 | -------------------------------------------------------------------------------- /core/server/apps/dependencies.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | Promise = require('bluebird'), 6 | spawn = require('child_process').spawn, 7 | win32 = process.platform === 'win32'; 8 | 9 | function AppDependencies(appPath) { 10 | this.appPath = appPath; 11 | } 12 | 13 | AppDependencies.prototype.install = function installAppDependencies() { 14 | var spawnOpts, 15 | self = this; 16 | 17 | return new Promise(function (resolve, reject) { 18 | fs.stat(path.join(self.appPath, 'package.json'), function (err) { 19 | if (err) { 20 | // File doesn't exist - nothing to do, resolve right away? 21 | resolve(); 22 | } else { 23 | // Run npm install in the app directory 24 | spawnOpts = { 25 | cwd: self.appPath 26 | }; 27 | 28 | self.spawnCommand('npm', ['install', '--production'], spawnOpts) 29 | .on('error', reject) 30 | .on('exit', function (err) { 31 | if (err) { 32 | reject(err); 33 | } 34 | 35 | resolve(); 36 | }); 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | // Normalize a command across OS and spawn it; taken from yeoman/generator 43 | AppDependencies.prototype.spawnCommand = function (command, args, opt) { 44 | var winCommand = win32 ? 'cmd' : command, 45 | winArgs = win32 ? ['/c'].concat(command, args) : args; 46 | 47 | opt = opt || {}; 48 | 49 | return spawn(winCommand, winArgs, _.defaults({stdio: 'inherit'}, opt)); 50 | }; 51 | 52 | module.exports = AppDependencies; 53 | -------------------------------------------------------------------------------- /core/server/apps/permissions.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | Promise = require('bluebird'), 3 | path = require('path'), 4 | parsePackageJson = require('../utils/parse-package-json'); 5 | 6 | function AppPermissions(appPath) { 7 | this.appPath = appPath; 8 | this.packagePath = path.join(this.appPath, 'package.json'); 9 | } 10 | 11 | AppPermissions.prototype.read = function () { 12 | var self = this; 13 | 14 | return this.checkPackageContentsExists().then(function (exists) { 15 | if (!exists) { 16 | // If no package.json, return default permissions 17 | return Promise.resolve(AppPermissions.DefaultPermissions); 18 | } 19 | 20 | // Read and parse the package.json 21 | return self.getPackageContents().then(function (parsed) { 22 | // If no permissions in the package.json then return the default permissions. 23 | if (!(parsed.ghost && parsed.ghost.permissions)) { 24 | return Promise.resolve(AppPermissions.DefaultPermissions); 25 | } 26 | 27 | // TODO: Validation on permissions object? 28 | 29 | return Promise.resolve(parsed.ghost.permissions); 30 | }); 31 | }); 32 | }; 33 | 34 | AppPermissions.prototype.checkPackageContentsExists = function () { 35 | var self = this; 36 | 37 | // Mostly just broken out for stubbing in unit tests 38 | return new Promise(function (resolve) { 39 | fs.stat(self.packagePath, function (err) { 40 | var exists = !err; 41 | resolve(exists); 42 | }); 43 | }); 44 | }; 45 | 46 | // Get the contents of the package.json in the appPath root 47 | AppPermissions.prototype.getPackageContents = function () { 48 | return parsePackageJson(this.packagePath); 49 | }; 50 | 51 | // Default permissions for an App. 52 | AppPermissions.DefaultPermissions = { 53 | posts: ['browse', 'read'] 54 | }; 55 | 56 | module.exports = AppPermissions; 57 | -------------------------------------------------------------------------------- /core/server/apps/private-blogging/index.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'), 2 | errors = require('../../errors'), 3 | i18n = require('../../i18n'), 4 | middleware = require('./lib/middleware'), 5 | router = require('./lib/router'); 6 | 7 | module.exports = { 8 | activate: function activate() { 9 | if (config.paths.subdir) { 10 | var paths = config.paths.subdir.split('/'); 11 | 12 | if (paths.pop() === config.routeKeywords.private) { 13 | errors.logErrorAndExit( 14 | new Error(i18n.t('errors.config.urlCannotContainPrivateSubdir.error')), 15 | i18n.t('errors.config.urlCannotContainPrivateSubdir.description'), 16 | i18n.t('errors.config.urlCannotContainPrivateSubdir.help') 17 | ); 18 | } 19 | } 20 | }, 21 | 22 | setupMiddleware: function setupMiddleware(blogApp) { 23 | blogApp.use(middleware.checkIsPrivate); 24 | blogApp.use(middleware.filterPrivateRoutes); 25 | }, 26 | 27 | setupRoutes: function setupRoutes(blogRouter) { 28 | blogRouter.use('/' + config.routeKeywords.private + '/', router); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /core/server/apps/private-blogging/lib/router.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | express = require('express'), 3 | middleware = require('./middleware'), 4 | templates = require('../../../controllers/frontend/templates'), 5 | setResponseContext = require('../../../controllers/frontend/context'), 6 | privateRouter = express.Router(); 7 | 8 | function controller(req, res) { 9 | var defaultView = path.resolve(__dirname, 'views', 'private.hbs'), 10 | paths = templates.getActiveThemePaths(req.app.get('activeTheme')), 11 | data = {}; 12 | 13 | if (res.error) { 14 | data.error = res.error; 15 | } 16 | 17 | setResponseContext(req, res); 18 | if (paths.hasOwnProperty('private.hbs')) { 19 | return res.render('private', data); 20 | } else { 21 | return res.render(defaultView, data); 22 | } 23 | } 24 | 25 | // password-protected frontend route 26 | privateRouter.route('/') 27 | .get( 28 | middleware.isPrivateSessionAuth, 29 | controller 30 | ) 31 | .post( 32 | middleware.isPrivateSessionAuth, 33 | middleware.spamPrevention, 34 | middleware.authenticateProtection, 35 | controller 36 | ); 37 | 38 | module.exports = privateRouter; 39 | module.exports.controller = controller; 40 | -------------------------------------------------------------------------------- /core/server/apps/private-blogging/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /core/server/controllers/frontend/channel-config.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../config'), 3 | channelConfig; 4 | 5 | channelConfig = function channelConfig() { 6 | var defaults = { 7 | index: { 8 | name: 'index', 9 | route: '/', 10 | frontPageTemplate: 'home' 11 | }, 12 | tag: { 13 | name: 'tag', 14 | route: '/' + config.routeKeywords.tag + '/:slug/', 15 | postOptions: { 16 | filter: 'tags:\'%s\'' 17 | }, 18 | data: { 19 | tag: { 20 | type: 'read', 21 | resource: 'tags', 22 | options: {slug: '%s'} 23 | } 24 | }, 25 | slugTemplate: true, 26 | editRedirect: '/ghost/settings/tags/:slug/' 27 | }, 28 | author: { 29 | name: 'author', 30 | route: '/' + config.routeKeywords.author + '/:slug/', 31 | postOptions: { 32 | filter: 'author:\'%s\'' 33 | }, 34 | data: { 35 | author: { 36 | type: 'read', 37 | resource: 'users', 38 | options: {slug: '%s'} 39 | } 40 | }, 41 | slugTemplate: true, 42 | editRedirect: '/ghost/team/:slug/' 43 | } 44 | }; 45 | 46 | return defaults; 47 | }; 48 | 49 | module.exports.list = function list() { 50 | return channelConfig(); 51 | }; 52 | 53 | module.exports.get = function get(name) { 54 | return _.cloneDeep(channelConfig()[name]); 55 | }; 56 | -------------------------------------------------------------------------------- /core/server/controllers/frontend/error.js: -------------------------------------------------------------------------------- 1 | function handleError(next) { 2 | return function handleError(err) { 3 | // If we've thrown an error message of type: 'NotFound' then we found no path match. 4 | if (err.errorType === 'NotFoundError') { 5 | return next(); 6 | } 7 | 8 | return next(err); 9 | }; 10 | } 11 | 12 | module.exports = handleError; 13 | -------------------------------------------------------------------------------- /core/server/controllers/frontend/format-response.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | /** 4 | * formats variables for handlebars in multi-post contexts. 5 | * If extraValues are available, they are merged in the final value 6 | * @return {Object} containing page variables 7 | */ 8 | function formatPageResponse(result) { 9 | var response = { 10 | posts: result.posts, 11 | pagination: result.meta.pagination 12 | }; 13 | 14 | _.each(result.data, function (data, name) { 15 | if (data.meta) { 16 | // Move pagination to be a top level key 17 | response[name] = data; 18 | response[name].pagination = data.meta.pagination; 19 | delete response[name].meta; 20 | } else { 21 | // This is a single object, don't wrap it in an array 22 | response[name] = data[0]; 23 | } 24 | }); 25 | 26 | return response; 27 | } 28 | 29 | /** 30 | * similar to formatPageResponse, but for single post pages 31 | * @return {Object} containing page variables 32 | */ 33 | function formatResponse(post) { 34 | return { 35 | post: post 36 | }; 37 | } 38 | 39 | module.exports = { 40 | channel: formatPageResponse, 41 | single: formatResponse 42 | }; 43 | -------------------------------------------------------------------------------- /core/server/controllers/frontend/secure.js: -------------------------------------------------------------------------------- 1 | // TODO: figure out how to remove the need for this 2 | // Add Request context parameter to the data object 3 | // to be passed down to the templates 4 | function setRequestIsSecure(req, data) { 5 | (Array.isArray(data) ? data : [data]).forEach(function forEach(d) { 6 | d.secure = req.secure; 7 | }); 8 | } 9 | 10 | module.exports = setRequestIsSecure; 11 | -------------------------------------------------------------------------------- /core/server/data/db/connection.js: -------------------------------------------------------------------------------- 1 | var knex = require('knex'), 2 | config = require('../../config'), 3 | knexInstance; 4 | 5 | // @TODO: 6 | // - if you require this file before config file was loaded, 7 | // - then this file is cached and you have no chance to connect to the db anymore 8 | // - bring dynamic into this file (db.connect()) 9 | function configure(dbConfig) { 10 | var client = dbConfig.client, 11 | pg; 12 | 13 | dbConfig.isPostgreSQL = function () { 14 | return client === 'pg' || client === 'postgres' || client === 'postgresql'; 15 | }; 16 | 17 | if (dbConfig.isPostgreSQL()) { 18 | try { 19 | pg = require('pg'); 20 | } catch (e) { 21 | pg = require('pg.js'); 22 | } 23 | 24 | // By default PostgreSQL returns data as strings along with an OID that identifies 25 | // its type. We're setting the parser to convert OID 20 (int8) into a javascript 26 | // integer. 27 | pg.types.setTypeParser(20, function (val) { 28 | return val === null ? null : parseInt(val, 10); 29 | }); 30 | 31 | // https://github.com/tgriesser/knex/issues/97 32 | // this sets the timezone to UTC only for the connection! 33 | dbConfig.pool = { 34 | afterCreate: function (connection, callback) { 35 | connection.query('set timezone=\'UTC\'', function (err) { 36 | callback(err, connection); 37 | }); 38 | } 39 | }; 40 | } 41 | 42 | if (client === 'sqlite3') { 43 | dbConfig.useNullAsDefault = dbConfig.useNullAsDefault || false; 44 | } 45 | 46 | if (client === 'mysql') { 47 | dbConfig.connection.timezone = 'UTC'; 48 | } 49 | 50 | return dbConfig; 51 | } 52 | 53 | if (!knexInstance && config.database && config.database.client) { 54 | knexInstance = knex(configure(config.database)); 55 | } 56 | 57 | module.exports = knexInstance; 58 | -------------------------------------------------------------------------------- /core/server/data/db/index.js: -------------------------------------------------------------------------------- 1 | var connection; 2 | 3 | Object.defineProperty(exports, 'knex', { 4 | enumerable: true, 5 | configurable: true, 6 | get: function get() { 7 | connection = connection || require('./connection'); 8 | return connection; 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /core/server/data/importer/handlers/image.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | path = require('path'), 4 | config = require('../../../config'), 5 | storage = require('../../../storage'), 6 | 7 | ImageHandler; 8 | 9 | ImageHandler = { 10 | type: 'images', 11 | extensions: config.uploads.extensions, 12 | types: config.uploads.contentTypes, 13 | directories: ['images', 'content'], 14 | 15 | loadFile: function (files, baseDir) { 16 | var store = storage.getStorage(), 17 | baseDirRegex = baseDir ? new RegExp('^' + baseDir + '/') : new RegExp(''), 18 | imageFolderRegexes = _.map(config.paths.imagesRelPath.split('/'), function (dir) { 19 | return new RegExp('^' + dir + '/'); 20 | }); 21 | 22 | // normalize the directory structure 23 | files = _.map(files, function (file) { 24 | var noBaseDir = file.name.replace(baseDirRegex, ''), 25 | noGhostDirs = noBaseDir; 26 | 27 | _.each(imageFolderRegexes, function (regex) { 28 | noGhostDirs = noGhostDirs.replace(regex, ''); 29 | }); 30 | 31 | file.originalPath = noBaseDir; 32 | file.name = noGhostDirs; 33 | file.targetDir = path.join(config.paths.imagesPath, path.dirname(noGhostDirs)); 34 | return file; 35 | }); 36 | 37 | return Promise.map(files, function (image) { 38 | return store.getUniqueFileName(store, image, image.targetDir).then(function (targetFilename) { 39 | image.newPath = (config.paths.subdir + '/' + 40 | config.paths.imagesRelPath + '/' + path.relative(config.paths.imagesPath, targetFilename)) 41 | .replace(new RegExp('\\' + path.sep, 'g'), '/'); 42 | return image; 43 | }); 44 | }); 45 | } 46 | }; 47 | 48 | module.exports = ImageHandler; 49 | -------------------------------------------------------------------------------- /core/server/data/importer/handlers/json.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | fs = require('fs-extra'), 4 | errors = require('../../../errors'), 5 | i18n = require('../../../i18n'), 6 | JSONHandler; 7 | 8 | JSONHandler = { 9 | type: 'data', 10 | extensions: ['.json'], 11 | types: ['application/octet-stream', 'application/json'], 12 | directories: [], 13 | 14 | loadFile: function (files, startDir) { 15 | /*jshint unused:false */ 16 | // @TODO: Handle multiple JSON files 17 | var filePath = files[0].path; 18 | 19 | return Promise.promisify(fs.readFile)(filePath).then(function (fileData) { 20 | var importData; 21 | try { 22 | importData = JSON.parse(fileData); 23 | 24 | // if importData follows JSON-API format `{ db: [exportedData] }` 25 | if (_.keys(importData).length === 1) { 26 | if (!importData.db || !Array.isArray(importData.db)) { 27 | throw new Error(i18n.t('errors.data.importer.handlers.json.invalidJsonFormat')); 28 | } 29 | 30 | importData = importData.db[0]; 31 | } 32 | 33 | return importData; 34 | } catch (e) { 35 | errors.logError(e, i18n.t('errors.data.importer.handlers.json.apiDbImportContent'), 36 | i18n.t('errors.data.importer.handlers.json.checkImportJsonIsValid')); 37 | return Promise.reject(new errors.BadRequestError(i18n.t('errors.data.importer.handlers.json.failedToParseImportJson'))); 38 | } 39 | }); 40 | } 41 | }; 42 | 43 | module.exports = JSONHandler; 44 | -------------------------------------------------------------------------------- /core/server/data/importer/importers/data.js: -------------------------------------------------------------------------------- 1 | var importer = require('../../import'), 2 | DataImporter; 3 | 4 | DataImporter = { 5 | type: 'data', 6 | preProcess: function (importData) { 7 | importData.preProcessedByData = true; 8 | return importData; 9 | }, 10 | doImport: function (importData) { 11 | return importer.doImport(importData); 12 | } 13 | }; 14 | 15 | module.exports = DataImporter; 16 | -------------------------------------------------------------------------------- /core/server/data/meta/asset_url.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'); 2 | 3 | function getAssetUrl(path, isAdmin, minify) { 4 | var output = ''; 5 | 6 | output += config.paths.subdir + '/'; 7 | 8 | if (!path.match(/^favicon\.ico$/) && !path.match(/^shared/) && !path.match(/^asset/)) { 9 | if (isAdmin) { 10 | output += 'ghost/'; 11 | } else { 12 | output += 'assets/'; 13 | } 14 | } 15 | 16 | // Get rid of any leading slash on the path 17 | path = path.replace(/^\//, ''); 18 | 19 | // replace ".foo" with ".min.foo" in production 20 | if (minify) { 21 | path = path.replace(/\.([^\.]*)$/, '.min.$1'); 22 | } 23 | 24 | output += path; 25 | 26 | if (!path.match(/^favicon\.ico$/)) { 27 | output = output + '?v=' + config.assetHash; 28 | } 29 | 30 | return output; 31 | } 32 | 33 | module.exports = getAssetUrl; 34 | -------------------------------------------------------------------------------- /core/server/data/meta/author_fb_url.js: -------------------------------------------------------------------------------- 1 | var getContextObject = require('./context_object.js'); 2 | 3 | function getAuthorFacebookUrl(data) { 4 | var context = data.context ? data.context[0] : null, 5 | contextObject = getContextObject(data, context); 6 | 7 | if ((context === 'post' || context === 'page') && contextObject.author && contextObject.author.facebook) { 8 | return contextObject.author.facebook; 9 | } else if (context === 'author' && contextObject.facebook) { 10 | return contextObject.facebook; 11 | } 12 | return null; 13 | } 14 | 15 | module.exports = getAuthorFacebookUrl; 16 | -------------------------------------------------------------------------------- /core/server/data/meta/author_image.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'), 2 | getContextObject = require('./context_object.js'); 3 | 4 | function getAuthorImage(data, absolute) { 5 | var context = data.context ? data.context[0] : null, 6 | contextObject = getContextObject(data, context); 7 | 8 | if ((context === 'post' || context === 'page') && contextObject.author && contextObject.author.image) { 9 | return config.urlFor('image', {image: contextObject.author.image}, absolute); 10 | } 11 | return null; 12 | } 13 | 14 | module.exports = getAuthorImage; 15 | -------------------------------------------------------------------------------- /core/server/data/meta/author_url.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'); 2 | 3 | function getAuthorUrl(data, absolute) { 4 | var context = data.context ? data.context[0] : null; 5 | if (data.author) { 6 | return config.urlFor('author', {author: data.author}, absolute); 7 | } 8 | if (data[context] && data[context].author) { 9 | return config.urlFor('author', {author: data[context].author}, absolute); 10 | } 11 | return null; 12 | } 13 | 14 | module.exports = getAuthorUrl; 15 | -------------------------------------------------------------------------------- /core/server/data/meta/canonical_url.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'), 2 | getUrl = require('./url'); 3 | 4 | function getCanonicalUrl(data) { 5 | return config.urlJoin(config.getBaseUrl(false), 6 | getUrl(data, false)); 7 | } 8 | 9 | module.exports = getCanonicalUrl; 10 | -------------------------------------------------------------------------------- /core/server/data/meta/context_object.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'); 2 | 3 | function getContextObject(data, context) { 4 | var blog = config.theme, 5 | contextObject; 6 | 7 | context = context === 'page' ? 'post' : context; 8 | contextObject = data[context] || blog; 9 | 10 | return contextObject; 11 | } 12 | 13 | module.exports = getContextObject; 14 | -------------------------------------------------------------------------------- /core/server/data/meta/cover_image.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'), 2 | getContextObject = require('./context_object.js'); 3 | 4 | function getCoverImage(data) { 5 | var context = data.context ? data.context[0] : null, 6 | contextObject = getContextObject(data, context); 7 | 8 | if (context === 'home' || context === 'author') { 9 | if (contextObject.cover) { 10 | return config.urlFor('image', {image: contextObject.cover}, true); 11 | } 12 | } else { 13 | if (contextObject.image) { 14 | return config.urlFor('image', {image: contextObject.image}, true); 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | module.exports = getCoverImage; 21 | -------------------------------------------------------------------------------- /core/server/data/meta/creator_url.js: -------------------------------------------------------------------------------- 1 | var getContextObject = require('./context_object.js'); 2 | 3 | function getCreatorTwitterUrl(data) { 4 | var context = data.context ? data.context[0] : null, 5 | contextObject = getContextObject(data, context); 6 | if ((context === 'post' || context === 'page') && contextObject.author && contextObject.author.twitter) { 7 | return contextObject.author.twitter; 8 | } else if (context === 'author' && contextObject.twitter) { 9 | return contextObject.twitter; 10 | } 11 | return null; 12 | } 13 | 14 | module.exports = getCreatorTwitterUrl; 15 | -------------------------------------------------------------------------------- /core/server/data/meta/description.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../config'); 3 | 4 | function getDescription(data, root) { 5 | var description = '', 6 | context = root ? root.context : null; 7 | 8 | if (data.meta_description) { 9 | description = data.meta_description; 10 | } else if (_.includes(context, 'paged')) { 11 | description = ''; 12 | } else if (_.includes(context, 'home')) { 13 | description = config.theme.description; 14 | } else if (_.includes(context, 'author') && data.author) { 15 | description = data.author.meta_description || data.author.bio; 16 | } else if (_.includes(context, 'tag') && data.tag) { 17 | description = data.tag.meta_description || data.tag.description; 18 | } else if ((_.includes(context, 'post') || _.includes(context, 'page')) && data.post) { 19 | description = data.post.meta_description; 20 | } 21 | 22 | return (description || '').trim(); 23 | } 24 | 25 | module.exports = getDescription; 26 | -------------------------------------------------------------------------------- /core/server/data/meta/excerpt.js: -------------------------------------------------------------------------------- 1 | var downsize = require('downsize'); 2 | 3 | function getExcerpt(html, truncateOptions) { 4 | truncateOptions = truncateOptions || {}; 5 | // Strip inline and bottom footnotes 6 | var excerpt = html.replace(/.*?<\/a>/gi, ''); 7 | excerpt = excerpt.replace(/
    .*?<\/ol><\/div>/, ''); 8 | // Strip other html 9 | excerpt = excerpt.replace(/<\/?[^>]+>/gi, ''); 10 | excerpt = excerpt.replace(/(\r\n|\n|\r)+/gm, ' '); 11 | /*jslint regexp:false */ 12 | 13 | if (!truncateOptions.words && !truncateOptions.characters) { 14 | truncateOptions.words = 50; 15 | } 16 | 17 | return downsize(excerpt, truncateOptions); 18 | } 19 | 20 | module.exports = getExcerpt; 21 | -------------------------------------------------------------------------------- /core/server/data/meta/keywords.js: -------------------------------------------------------------------------------- 1 | function getKeywords(data) { 2 | if (data.post && data.post.tags && data.post.tags.length > 0) { 3 | return data.post.tags.map(function (tag) { 4 | return tag.name; 5 | }); 6 | } 7 | return null; 8 | } 9 | 10 | module.exports = getKeywords; 11 | -------------------------------------------------------------------------------- /core/server/data/meta/modified_date.js: -------------------------------------------------------------------------------- 1 | function getModifiedDate(data) { 2 | var context = data.context ? data.context[0] : null, 3 | modDate; 4 | if (data[context]) { 5 | modDate = data[context].updated_at || null; 6 | if (modDate) { 7 | return new Date(modDate).toISOString(); 8 | } 9 | } 10 | return null; 11 | } 12 | 13 | module.exports = getModifiedDate; 14 | -------------------------------------------------------------------------------- /core/server/data/meta/og_type.js: -------------------------------------------------------------------------------- 1 | function getOgType(data) { 2 | var context = data.context ? data.context[0] : null; 3 | if (context === 'author') { 4 | return 'profile'; 5 | } 6 | if (context === 'post') { 7 | return 'article'; 8 | } 9 | return 'website'; 10 | } 11 | 12 | module.exports = getOgType; 13 | -------------------------------------------------------------------------------- /core/server/data/meta/paginated_url.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../config'); 3 | 4 | function getPaginatedUrl(page, data, absolute) { 5 | // If we don't have enough information, return null right away 6 | if (!data || !data.relativeUrl || !data.pagination) { 7 | return null; 8 | } 9 | 10 | var pagePath = '/' + config.routeKeywords.page + '/', 11 | // Try to match the base url, as whatever precedes the pagePath 12 | baseUrlPattern = new RegExp('(.+)?(/' + config.routeKeywords.page + '/\\d+/)'), 13 | baseUrlMatch = data.relativeUrl.match(baseUrlPattern), 14 | // If there is no match for pagePath, use the original url, without the trailing slash 15 | baseUrl = baseUrlMatch ? baseUrlMatch[1] : data.relativeUrl.slice(0, -1), 16 | newRelativeUrl; 17 | 18 | if (page === 'next' && data.pagination.next) { 19 | newRelativeUrl = pagePath + data.pagination.next + '/'; 20 | } else if (page === 'prev' && data.pagination.prev) { 21 | newRelativeUrl = data.pagination.prev > 1 ? pagePath + data.pagination.prev + '/' : '/'; 22 | } else if (_.isNumber(page)) { 23 | newRelativeUrl = page > 1 ? pagePath + page + '/' : '/'; 24 | } else { 25 | // If none of the cases match, return null right away 26 | return null; 27 | } 28 | 29 | // baseUrl can be undefined, if there was nothing preceding the pagePath (e.g. first page of the index channel) 30 | newRelativeUrl = baseUrl ? baseUrl + newRelativeUrl : newRelativeUrl; 31 | 32 | return config.urlFor({relativeUrl: newRelativeUrl, secure: data.secure}, absolute); 33 | } 34 | 35 | module.exports = getPaginatedUrl; 36 | -------------------------------------------------------------------------------- /core/server/data/meta/published_date.js: -------------------------------------------------------------------------------- 1 | function getPublishedDate(data) { 2 | var context = data.context ? data.context[0] : null; 3 | if (data[context] && data[context].published_at) { 4 | return new Date(data[context].published_at).toISOString(); 5 | } 6 | return null; 7 | } 8 | 9 | module.exports = getPublishedDate; 10 | -------------------------------------------------------------------------------- /core/server/data/meta/rss_url.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config'); 2 | 3 | function getRssUrl(data, absolute) { 4 | return config.urlFor('rss', {secure: data.secure}, absolute); 5 | } 6 | 7 | module.exports = getRssUrl; 8 | -------------------------------------------------------------------------------- /core/server/data/meta/structured_data.js: -------------------------------------------------------------------------------- 1 | var socialUrls = require('../../utils/social-urls'); 2 | 3 | function getStructuredData(metaData) { 4 | var structuredData, 5 | card = 'summary'; 6 | 7 | if (metaData.coverImage) { 8 | card = 'summary_large_image'; 9 | } 10 | 11 | structuredData = { 12 | 'og:site_name': metaData.blog.title, 13 | 'og:type': metaData.ogType, 14 | 'og:title': metaData.metaTitle, 15 | 'og:description': metaData.metaDescription || metaData.excerpt, 16 | 'og:url': metaData.canonicalUrl, 17 | 'og:image': metaData.coverImage, 18 | 'article:published_time': metaData.publishedDate, 19 | 'article:modified_time': metaData.modifiedDate, 20 | 'article:tag': metaData.keywords, 21 | 'article:publisher': metaData.blog.facebook ? socialUrls.facebookUrl(metaData.blog.facebook) : undefined, 22 | 'article:author': metaData.authorFacebook ? socialUrls.facebookUrl(metaData.authorFacebook) : undefined, 23 | 'twitter:card': card, 24 | 'twitter:title': metaData.metaTitle, 25 | 'twitter:description': metaData.metaDescription || metaData.excerpt, 26 | 'twitter:url': metaData.canonicalUrl, 27 | 'twitter:image': metaData.coverImage, 28 | 'twitter:label1': metaData.authorName ? 'Written by' : undefined, 29 | 'twitter:data1': metaData.authorName, 30 | 'twitter:label2': metaData.keywords ? 'Filed under' : undefined, 31 | 'twitter:data2': metaData.keywords ? metaData.keywords.join(', ') : undefined, 32 | 'twitter:site': metaData.blog.twitter || undefined, 33 | 'twitter:creator': metaData.creatorTwitter || undefined 34 | }; 35 | 36 | // return structured data removing null or undefined keys 37 | return Object.keys(structuredData).reduce(function (data, key) { 38 | var content = structuredData[key]; 39 | if (content !== null && typeof content !== 'undefined') { 40 | data[key] = content; 41 | } 42 | return data; 43 | }, {}); 44 | } 45 | 46 | module.exports = getStructuredData; 47 | -------------------------------------------------------------------------------- /core/server/data/meta/title.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | config = require('../../config'); 3 | 4 | function getTitle(data, root) { 5 | var title = '', 6 | context = root ? root.context : null, 7 | blog = config.theme, 8 | pagination = root ? root.pagination : null, 9 | pageString = ''; 10 | 11 | if (pagination && pagination.total > 1) { 12 | pageString = ' - Page ' + pagination.page; 13 | } 14 | if (data.meta_title) { 15 | title = data.meta_title; 16 | } else if (_.includes(context, 'home')) { 17 | title = blog.title; 18 | } else if (_.includes(context, 'author') && data.author) { 19 | title = data.author.name + pageString + ' - ' + blog.title; 20 | } else if (_.includes(context, 'tag') && data.tag) { 21 | title = data.tag.meta_title || data.tag.name + pageString + ' - ' + blog.title; 22 | } else if ((_.includes(context, 'post') || _.includes(context, 'page')) && data.post) { 23 | title = data.post.meta_title || data.post.title; 24 | } else { 25 | title = blog.title + pageString; 26 | } 27 | 28 | return (title || '').trim(); 29 | } 30 | 31 | module.exports = getTitle; 32 | -------------------------------------------------------------------------------- /core/server/data/meta/url.js: -------------------------------------------------------------------------------- 1 | var schema = require('../schema').checks, 2 | config = require('../../config'); 3 | 4 | function getUrl(data, absolute) { 5 | if (schema.isPost(data)) { 6 | return config.urlFor('post', {post: data, secure: data.secure}, absolute); 7 | } 8 | 9 | if (schema.isTag(data)) { 10 | return config.urlFor('tag', {tag: data, secure: data.secure}, absolute); 11 | } 12 | 13 | if (schema.isUser(data)) { 14 | return config.urlFor('author', {author: data, secure: data.secure}, absolute); 15 | } 16 | 17 | if (schema.isNav(data)) { 18 | return config.urlFor('nav', {nav: data, secure: data.secure}, absolute); 19 | } 20 | 21 | return config.urlFor(data, {}, absolute); 22 | } 23 | 24 | module.exports = getUrl; 25 | -------------------------------------------------------------------------------- /core/server/data/migration/004/01-add-tour-column-to-users.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'users', 4 | column = 'tour', 5 | message = 'Adding column: ' + table + '.' + column; 6 | 7 | module.exports = function addTourColumnToUsers(options, logger) { 8 | var transaction = options.transacting; 9 | 10 | return transaction.schema.hasTable(table) 11 | .then(function (exists) { 12 | if (!exists) { 13 | return Promise.reject(new Error('Table does not exist!')); 14 | } 15 | 16 | return transaction.schema.hasColumn(table, column); 17 | }) 18 | .then(function (exists) { 19 | if (!exists) { 20 | logger.info(message); 21 | return commands.addColumn(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/004/02-add-sortorder-column-to-poststags.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'posts_tags', 4 | column = 'sort_order', 5 | message = 'Adding column: ' + table + '.' + column; 6 | 7 | module.exports = function addSortOrderColumnToPostsTags(options, logger) { 8 | var transaction = options.transacting; 9 | 10 | return transaction.schema.hasTable(table) 11 | .then(function (exists) { 12 | if (!exists) { 13 | return Promise.reject(new Error('Table does not exist!')); 14 | } 15 | 16 | return transaction.schema.hasColumn(table, column); 17 | }) 18 | .then(function (exists) { 19 | if (!exists) { 20 | logger.info(message); 21 | return commands.addColumn(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/004/03-add-many-columns-to-clients.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'clients', 4 | columns = ['redirection_uri', 'logo', 'status', 'type', 'description']; 5 | 6 | module.exports = function addManyColumnsToClients(options, logger) { 7 | var transaction = options.transacting; 8 | 9 | return transaction.schema.hasTable(table) 10 | .then(function (exists) { 11 | if (!exists) { 12 | return Promise.reject(new Error('Table does not exist!')); 13 | } 14 | 15 | return Promise.mapSeries(columns, function (column) { 16 | var message = 'Adding column: ' + table + '.' + column; 17 | 18 | return transaction.schema.hasColumn(table, column) 19 | .then(function (exists) { 20 | if (!exists) { 21 | logger.info(message); 22 | return commands.addColumn(table, column, transaction); 23 | } else { 24 | logger.warn(message); 25 | } 26 | }); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /core/server/data/migration/004/04-add-clienttrusteddomains-table.js: -------------------------------------------------------------------------------- 1 | var commands = require('../../schema').commands, 2 | table = 'client_trusted_domains', 3 | message = 'Creating table: ' + table; 4 | 5 | module.exports = function addClientTrustedDomainsTable(options, logger) { 6 | var transaction = options.transacting; 7 | 8 | return transaction.schema.hasTable(table) 9 | .then(function (exists) { 10 | if (!exists) { 11 | logger.info(message); 12 | return commands.createTable(table, transaction); 13 | } else { 14 | logger.warn(message); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /core/server/data/migration/004/05-drop-unique-on-clients-secret.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'clients', 4 | column = 'secret', 5 | message = 'Dropping unique on: ' + table + '.' + column; 6 | 7 | module.exports = function dropUniqueOnClientsSecret(options, logger) { 8 | var transaction = options.transacting; 9 | 10 | return transaction.schema.hasTable(table) 11 | .then(function (exists) { 12 | if (!exists) { 13 | return Promise.reject(new Error('Table does not exist!')); 14 | } 15 | 16 | return commands.getIndexes(table, transaction); 17 | }) 18 | .then(function (indexes) { 19 | if (indexes.indexOf(table + '_' + column + '_unique') > -1) { 20 | logger.info(message); 21 | return commands.dropUnique(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/004/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Added tour column to users 3 | require('./01-add-tour-column-to-users'), 4 | // Added sort_order to posts_tags 5 | require('./02-add-sortorder-column-to-poststags'), 6 | // Added redirection_uri, logo, status, type & description columns to clients 7 | require('./03-add-many-columns-to-clients'), 8 | // Added client_trusted_domains table 9 | require('./04-add-clienttrusteddomains-table'), 10 | // Dropped unique index on client secret 11 | require('./05-drop-unique-on-clients-secret') 12 | ]; 13 | -------------------------------------------------------------------------------- /core/server/data/migration/005/01-drop-hidden-column-from-tags.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'tags', 4 | column = 'hidden', 5 | message = 'Removing column: ' + table + '.' + column; 6 | 7 | module.exports = function dropHiddenColumnFromTags(options, logger) { 8 | var transaction = options.transacting; 9 | 10 | return transaction.schema.hasTable(table) 11 | .then(function (exists) { 12 | if (!exists) { 13 | return Promise.reject(new Error('Table does not exist!')); 14 | } 15 | 16 | return transaction.schema.hasColumn(table, column); 17 | }) 18 | .then(function (exists) { 19 | if (exists) { 20 | logger.info(message); 21 | return commands.dropColumn(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/005/02-add-visibility-column-to-key-tables.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | tables = ['posts', 'tags', 'users'], 4 | column = 'visibility'; 5 | 6 | module.exports = function addVisibilityColumnToKeyTables(options, logger) { 7 | var transaction = options.transacting; 8 | 9 | return Promise.mapSeries(tables, function (table) { 10 | var message = 'Adding column: ' + table + '.' + column; 11 | 12 | return transaction.schema.hasTable(table) 13 | .then(function (exists) { 14 | if (!exists) { 15 | return Promise.reject(new Error('Table does not exist!')); 16 | } 17 | 18 | return transaction.schema.hasColumn(table, column); 19 | }) 20 | .then(function (exists) { 21 | if (!exists) { 22 | logger.info(message); 23 | return commands.addColumn(table, column, transaction); 24 | } else { 25 | logger.warn(message); 26 | } 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /core/server/data/migration/005/03-add-mobiledoc-column-to-posts.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'posts', 4 | column = 'mobiledoc', 5 | message = 'Adding column: ' + table + '.' + column; 6 | 7 | module.exports = function addMobiledocColumnToPosts(options, logger) { 8 | var transaction = options.transacting; 9 | 10 | return transaction.schema.hasTable(table) 11 | .then(function (exists) { 12 | if (!exists) { 13 | return Promise.reject(new Error('Table does not exist!')); 14 | } 15 | 16 | return transaction.schema.hasColumn(table, column); 17 | }) 18 | .then(function (exists) { 19 | if (!exists) { 20 | logger.info(message); 21 | return commands.addColumn(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/005/04-add-social-media-columns-to-users.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | commands = require('../../schema').commands, 3 | table = 'users', 4 | columns = ['facebook', 'twitter']; 5 | 6 | module.exports = function addSocialMediaColumnsToUsers(options, logger) { 7 | var transaction = options.transacting; 8 | 9 | return transaction.schema.hasTable(table) 10 | .then(function (exists) { 11 | if (!exists) { 12 | return Promise.reject(new Error('Table does not exist!')); 13 | } 14 | 15 | return Promise.mapSeries(columns, function (column) { 16 | var message = 'Adding column: ' + table + '.' + column; 17 | 18 | return transaction.schema.hasColumn(table, column).then(function (exists) { 19 | if (!exists) { 20 | logger.info(message); 21 | return commands.addColumn(table, column, transaction); 22 | } else { 23 | logger.warn(message); 24 | } 25 | }); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /core/server/data/migration/005/05-add-subscribers-table.js: -------------------------------------------------------------------------------- 1 | var commands = require('../../schema').commands, 2 | table = 'subscribers', 3 | message = 'Creating table: ' + table; 4 | 5 | module.exports = function addSubscribersTable(options, logger) { 6 | var transaction = options.transacting; 7 | 8 | return transaction.schema.hasTable(table).then(function (exists) { 9 | if (!exists) { 10 | logger.info(message); 11 | return commands.createTable(table, transaction); 12 | } else { 13 | logger.warn(message); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /core/server/data/migration/005/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Drop hidden column from tags table 3 | require('./01-drop-hidden-column-from-tags'), 4 | // Add visibility column to posts, tags, and users tables 5 | require('./02-add-visibility-column-to-key-tables'), 6 | // Add mobiledoc column to posts 7 | require('./03-add-mobiledoc-column-to-posts'), 8 | // Add social media columns to users 9 | require('./04-add-social-media-columns-to-users'), 10 | // Add subscribers table 11 | require('./05-add-subscribers-table') 12 | ]; 13 | -------------------------------------------------------------------------------- /core/server/data/migration/006/index.js: -------------------------------------------------------------------------------- 1 | module.exports = []; 2 | -------------------------------------------------------------------------------- /core/server/data/migration/backup.js: -------------------------------------------------------------------------------- 1 | // # Backup Database 2 | // Provides for backing up the database before making potentially destructive changes 3 | var _ = require('lodash'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | Promise = require('bluebird'), 7 | config = require('../../config'), 8 | exporter = require('../export'), 9 | 10 | writeExportFile, 11 | backup; 12 | 13 | writeExportFile = function writeExportFile(exportResult) { 14 | var filename = path.resolve(config.paths.contentPath + '/data/' + exportResult.filename); 15 | 16 | return Promise.promisify(fs.writeFile)(filename, JSON.stringify(exportResult.data)).return(filename); 17 | }; 18 | 19 | /** 20 | * ## Backup 21 | * does an export, and stores this in a local file 22 | * 23 | * @param {{info: logger.info, warn: logger.warn}} [logger] 24 | * @returns {Promise<*>} 25 | */ 26 | backup = function backup(logger) { 27 | // If we get passed a function, use it to output notices, else don't do anything 28 | logger = logger && _.isFunction(logger.info) ? logger : {info: _.noop}; 29 | 30 | logger.info('Creating database backup'); 31 | 32 | var props = { 33 | data: exporter.doExport(), 34 | filename: exporter.fileName() 35 | }; 36 | 37 | return Promise.props(props) 38 | .then(writeExportFile) 39 | .then(function successMessage(filename) { 40 | logger.info('Database backup written to: ' + filename); 41 | }); 42 | }; 43 | 44 | module.exports = backup; 45 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/01-move-jquery-with-alert.js: -------------------------------------------------------------------------------- 1 | // Moves jQuery inclusion to code injection via ghost_foot 2 | var _ = require('lodash'), 3 | Promise = require('bluebird'), 4 | serverPath = '../../../../', 5 | config = require(serverPath + 'config'), 6 | models = require(serverPath + 'models'), 7 | notifications = require(serverPath + 'api/notifications'), 8 | i18n = require(serverPath + 'i18n'), 9 | 10 | // These messages are shown in the admin UI, not the console, and should therefore be translated 11 | jquery = [ 12 | i18n.t('notices.data.fixtures.canSafelyDelete'), 13 | '\n\n' 14 | ], 15 | privacyMessage = [ 16 | i18n.t('notices.data.fixtures.jQueryRemoved'), 17 | i18n.t('notices.data.fixtures.canBeChanged') 18 | ], 19 | 20 | message = 'Adding jQuery link to ghost_foot'; 21 | 22 | module.exports = function moveJQuery(options, logger) { 23 | var value; 24 | 25 | return models.Settings.findOne('ghost_foot', options) 26 | .then(function (setting) { 27 | if (setting) { 28 | value = setting.get('value'); 29 | 30 | // Only add jQuery if it's not already in there 31 | if (value.indexOf(jquery.join('')) === -1) { 32 | logger.info(message); 33 | value = jquery.join('') + value; 34 | 35 | return models.Settings.edit({key: 'ghost_foot', value: value}, options); 36 | } else { 37 | logger.warn(message); 38 | } 39 | } else { 40 | logger.warn(message); 41 | } 42 | }) 43 | .then(function () { 44 | if (_.isEmpty(config.privacy)) { 45 | return Promise.resolve(); 46 | } 47 | 48 | logger.info(privacyMessage.join(' ').replace(/<\/?strong>/g, '')); 49 | 50 | return notifications.add({ 51 | notifications: [{ 52 | type: 'info', 53 | message: privacyMessage.join(' ') 54 | }] 55 | }, options); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/02-update-private-setting-type.js: -------------------------------------------------------------------------------- 1 | // Update the `isPrivate` setting, so that it has a type of `private` rather than `blog` 2 | var models = require('../../../../models'), 3 | 4 | message = 'Update isPrivate setting'; 5 | 6 | module.exports = function updatePrivateSetting(options, logger) { 7 | return models.Settings.findOne('isPrivate', options).then(function (setting) { 8 | if (setting && setting.get('type') !== 'private') { 9 | logger.info(message); 10 | return models.Settings.edit({key: 'isPrivate', type: 'private'}, options); 11 | } else { 12 | logger.warn(message); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/03-update-password-setting-type.js: -------------------------------------------------------------------------------- 1 | // Update the `password` setting, so that it has a type of `private` rather than `blog` 2 | var models = require('../../../../models'), 3 | message = 'Update password setting'; 4 | 5 | module.exports = function updatePasswordSetting(options, logger) { 6 | return models.Settings.findOne('password', options).then(function (setting) { 7 | if (setting && setting.get('type') !== 'private') { 8 | logger.info(message); 9 | return models.Settings.edit({key: 'password', type: 'private'}, options); 10 | } else { 11 | logger.warn(message); 12 | } 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/04-update-ghost-admin-client.js: -------------------------------------------------------------------------------- 1 | // Update the `ghost-admin` client so that it has a proper secret 2 | var _ = require('lodash'), 3 | Promise = require('bluebird'), 4 | crypto = require('crypto'), 5 | models = require('../../../../models'), 6 | adminClient = require('../utils').findModelFixtureEntry('Client', {slug: 'ghost-admin'}), 7 | message = 'Update ghost-admin client fixture'; 8 | 9 | module.exports = function updateGhostAdminClient(options, logger) { 10 | // ghost-admin should already exist from 003 version 11 | return models.Client.findOne({slug: adminClient.slug}, options) 12 | .then(function (client) { 13 | if (!client) { 14 | return Promise.reject(new Error('Admin client does not exist!')); 15 | } 16 | 17 | if (client.get('secret') === 'not_available' || client.get('status') !== 'enabled') { 18 | logger.info(message); 19 | return models.Client.edit( 20 | _.extend({}, adminClient, {secret: crypto.randomBytes(6).toString('hex')}), 21 | _.extend({}, options, {id: client.id}) 22 | ); 23 | } else { 24 | logger.warn(message); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/05-add-ghost-frontend-client.js: -------------------------------------------------------------------------------- 1 | // Create a new `ghost-frontend` client for use in themes 2 | var models = require('../../../../models'), 3 | frontendClient = require('../utils').findModelFixtureEntry('Client', {slug: 'ghost-frontend'}), 4 | message = 'Add ghost-frontend client fixture'; 5 | 6 | module.exports = function addGhostFrontendClient(options, logger) { 7 | return models.Client.findOne({slug: frontendClient.slug}, options) 8 | .then(function (client) { 9 | if (!client) { 10 | logger.info(message); 11 | return models.Client.add(frontendClient, options); 12 | } else { 13 | logger.warn(message); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/06-clean-broken-tags.js: -------------------------------------------------------------------------------- 1 | // Clean tags which start with commas, the only illegal char in tags 2 | var models = require('../../../../models'), 3 | Promise = require('bluebird'), 4 | message = 'Cleaning malformed tags'; 5 | 6 | module.exports = function cleanBrokenTags(options, logger) { 7 | return models.Tag.findAll(options).then(function (tags) { 8 | var tagOps = []; 9 | 10 | if (tags) { 11 | tags.each(function (tag) { 12 | var name = tag.get('name'), 13 | updated = name.replace(/^(,+)/, '').trim(); 14 | 15 | // If we've ended up with an empty string, default to just 'tag' 16 | updated = updated === '' ? 'tag' : updated; 17 | 18 | if (name !== updated) { 19 | tagOps.push(tag.save({name: updated}, options)); 20 | } 21 | }); 22 | if (tagOps.length > 0) { 23 | logger.info(message + '(' + tagOps.length + ')'); 24 | return Promise.all(tagOps); 25 | } else { 26 | logger.warn(message); 27 | } 28 | } else { 29 | logger.warn(message); 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/07-add-post-tag-order.js: -------------------------------------------------------------------------------- 1 | // Add a new order value to posts_tags based on the existing info 2 | var models = require('../../../../models'), 3 | _ = require('lodash'), 4 | sequence = require('../../../../utils/sequence'), 5 | migrationHasRunFlag, 6 | modelOptions; 7 | 8 | function loadTagsForEachPost(posts) { 9 | if (!posts) { 10 | return []; 11 | } 12 | 13 | return posts.mapThen(function loadTagsForPost(post) { 14 | return post.load(['tags'], modelOptions); 15 | }); 16 | } 17 | 18 | function updatePostTagsSortOrder(post, tagId, order) { 19 | var sortOrder = order; 20 | return function doUpdatePivot() { 21 | return post.tags().updatePivot( 22 | {sort_order: sortOrder}, _.extend({}, modelOptions, {query: {where: {tag_id: tagId}}}) 23 | ); 24 | }; 25 | } 26 | 27 | function buildTagOpsArray(tagOps, post) { 28 | var order = 0; 29 | 30 | return post.related('tags').reduce(function processTag(tagOps, tag) { 31 | if (tag.pivot.get('sort_order') > 0) { 32 | // if any entry in the posts_tags table has already run, we shouldn't run this again 33 | migrationHasRunFlag = true; 34 | } 35 | 36 | tagOps.push(updatePostTagsSortOrder(post, tag.id, order)); 37 | order += 1; 38 | 39 | return tagOps; 40 | }, tagOps); 41 | } 42 | 43 | function processPostsArray(postsArray) { 44 | return postsArray.reduce(buildTagOpsArray, []); 45 | } 46 | 47 | module.exports = function addPostTagOrder(options, logger) { 48 | modelOptions = options; 49 | migrationHasRunFlag = false; 50 | 51 | logger.info('Collecting data on tag order for posts...'); 52 | return models.Post.findAll(_.extend({}, modelOptions)) 53 | .then(loadTagsForEachPost) 54 | .then(processPostsArray) 55 | .then(function (tagOps) { 56 | if (tagOps.length > 0 && !migrationHasRunFlag) { 57 | logger.info('Updating order on ' + tagOps.length + ' tag relationships (could take a while)...'); 58 | return sequence(tagOps).then(function () { 59 | logger.info('Tag order successfully updated'); 60 | }); 61 | } else { 62 | logger.warn('Updating order on tag relationships'); 63 | } 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/004/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // add jquery setting and privacy info 3 | require('./01-move-jquery-with-alert'), 4 | 5 | // change `type` for protected blog `isPrivate` setting 6 | require('./02-update-private-setting-type'), 7 | 8 | // change `type` for protected blog `password` setting 9 | require('./03-update-password-setting-type'), 10 | 11 | // Update ghost-admin client fixture 12 | require('./04-update-ghost-admin-client'), 13 | 14 | // add ghost-frontend client if missing 15 | require('./05-add-ghost-frontend-client'), 16 | 17 | // clean up broken tags 18 | require('./06-clean-broken-tags'), 19 | 20 | // Add post_tag order 21 | require('./07-add-post-tag-order'), 22 | 23 | // Add a new draft post 24 | require('./08-add-post-fixture') 25 | ]; 26 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/005/01-update-ghost-client-secrets.js: -------------------------------------------------------------------------------- 1 | // Update the `ghost-*` clients so that they definitely have a proper secret 2 | var models = require('../../../../models'), 3 | _ = require('lodash'), 4 | Promise = require('bluebird'), 5 | crypto = require('crypto'), 6 | 7 | message = 'Updating client secret'; 8 | 9 | module.exports = function updateGhostClientsSecrets(options, logger) { 10 | return models.Clients.forge().query('where', 'secret', '=', 'not_available').fetch(options).then(function (results) { 11 | if (results.models.length === 0) { 12 | logger.warn(message); 13 | return; 14 | } 15 | 16 | return Promise.map(results.models, function mapper(client) { 17 | logger.info(message + ' (' + client.slug + ')'); 18 | client.secret = crypto.randomBytes(6).toString('hex'); 19 | 20 | return models.Client.edit( 21 | _.extend({}, client, {secret: crypto.randomBytes(6).toString('hex')}), 22 | _.extend({}, options, {id: client.id}) 23 | ); 24 | }); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/005/02-add-ghost-scheduler-client.js: -------------------------------------------------------------------------------- 1 | // Create a new `ghost-scheduler` client for use in themes 2 | var models = require('../../../../models'), 3 | 4 | schedulerClient = require('../utils').findModelFixtureEntry('Client', {slug: 'ghost-scheduler'}), 5 | message = 'Add ghost-scheduler client fixture'; 6 | 7 | module.exports = function addGhostFrontendClient(options, logger) { 8 | return models.Client.findOne({slug: schedulerClient.slug}, options).then(function (client) { 9 | if (!client) { 10 | logger.info(message); 11 | return models.Client.add(schedulerClient, options); 12 | } else { 13 | logger.warn(message); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/005/03-add-client-permissions.js: -------------------------------------------------------------------------------- 1 | // Update the permissions & permissions_roles tables to add entries for clients 2 | var utils = require('../utils'), 3 | resource = 'client'; 4 | 5 | function getPermissions() { 6 | return utils.findModelFixtures('Permission', {object_type: resource}); 7 | } 8 | 9 | function getRelations() { 10 | return utils.findPermissionRelationsForObject(resource); 11 | } 12 | 13 | function printResult(logger, result, message) { 14 | if (result.done === result.expected) { 15 | logger.info(message); 16 | } else { 17 | logger.warn('(' + result.done + '/' + result.expected + ') ' + message); 18 | } 19 | } 20 | 21 | module.exports = function addClientPermissions(options, logger) { 22 | var modelToAdd = getPermissions(), 23 | relationToAdd = getRelations(); 24 | 25 | return utils.addFixturesForModel(modelToAdd, options).then(function (result) { 26 | printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's'); 27 | return utils.addFixturesForRelation(relationToAdd, options); 28 | }).then(function (result) { 29 | printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's'); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/005/04-add-subscriber-permissions.js: -------------------------------------------------------------------------------- 1 | // Update the permissions & permissions_roles tables to add entries for subscribers 2 | var utils = require('../utils'), 3 | resource = 'subscriber'; 4 | 5 | function getPermissions() { 6 | return utils.findModelFixtures('Permission', {object_type: resource}); 7 | } 8 | 9 | function getRelations() { 10 | return utils.findPermissionRelationsForObject(resource); 11 | } 12 | 13 | function printResult(logger, result, message) { 14 | if (result.done === result.expected) { 15 | logger.info(message); 16 | } else { 17 | logger.warn('(' + result.done + '/' + result.expected + ') ' + message); 18 | } 19 | } 20 | 21 | module.exports = function addSubscriberPermissions(options, logger) { 22 | var modelToAdd = getPermissions(), 23 | relationToAdd = getRelations(); 24 | 25 | return utils.addFixturesForModel(modelToAdd, options).then(function (result) { 26 | printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's'); 27 | return utils.addFixturesForRelation(relationToAdd, options); 28 | }).then(function (result) { 29 | printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's'); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/005/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // add jquery setting and privacy info 3 | require('./01-update-ghost-client-secrets'), 4 | // add ghost-scheduler client 5 | require('./02-add-ghost-scheduler-client'), 6 | // add client permissions and permission_role relations 7 | require('./03-add-client-permissions'), 8 | // add subscriber permissions and permission_role relations 9 | require('./04-add-subscriber-permissions') 10 | ]; 11 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/006/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./01-transform-dates-into-utc') 3 | ]; 4 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/index.js: -------------------------------------------------------------------------------- 1 | var populate = require('./populate'), 2 | update = require('./update'), 3 | fixtures = require('./fixtures'); 4 | 5 | module.exports = { 6 | populate: populate, 7 | update: update, 8 | fixtures: fixtures 9 | }; 10 | -------------------------------------------------------------------------------- /core/server/data/migration/fixtures/update.js: -------------------------------------------------------------------------------- 1 | // # Update Fixtures 2 | // This module handles updating fixtures. 3 | // This is done manually, through a series of files stored in an adjacent folder 4 | // E.g. if we update to version 004, all the tasks in /004/ are executed 5 | 6 | var Promise = require('bluebird'), 7 | sequence = require('../../../utils/sequence'), 8 | update; 9 | 10 | /** 11 | * Handles doing subsequent update for one version 12 | */ 13 | update = function update(tasks, logger, modelOptions) { 14 | logger.info('Running fixture updates'); 15 | 16 | if (!tasks.length) { 17 | return Promise.resolve(); 18 | } 19 | 20 | return sequence(tasks, modelOptions, logger); 21 | }; 22 | 23 | module.exports = update; 24 | -------------------------------------------------------------------------------- /core/server/data/migration/index.js: -------------------------------------------------------------------------------- 1 | exports.update = require('./update'); 2 | exports.populate = require('./populate'); 3 | exports.reset = require('./reset'); 4 | exports.backupDatabase = require('./backup'); 5 | -------------------------------------------------------------------------------- /core/server/data/migration/populate.js: -------------------------------------------------------------------------------- 1 | // # Populate 2 | // Create a brand new database for a new install of ghost 3 | var Promise = require('bluebird'), 4 | commands = require('../schema').commands, 5 | fixtures = require('./fixtures'), 6 | errors = require('../../errors'), 7 | schema = require('../schema').tables, 8 | schemaTables = Object.keys(schema), 9 | populate, logger; 10 | 11 | // @TODO: remove me asap! 12 | logger = { 13 | info: function info(message) { 14 | errors.logComponentInfo('Migrations', message); 15 | }, 16 | warn: function warn(message) { 17 | errors.logComponentWarn('Skipping Migrations', message); 18 | } 19 | }; 20 | 21 | /** 22 | * ## Populate 23 | * Uses the schema to determine table structures, and automatically creates each table in order 24 | */ 25 | populate = function populate(options) { 26 | options = options || {}; 27 | 28 | var tablesOnly = options.tablesOnly, 29 | modelOptions = { 30 | context: { 31 | internal: true 32 | } 33 | }, 34 | tableSequence = Promise.mapSeries(schemaTables, function createTable(table) { 35 | logger.info('Creating table: ' + table); 36 | return commands.createTable(table); 37 | }); 38 | 39 | logger.info('Creating tables...'); 40 | 41 | if (tablesOnly) { 42 | return tableSequence; 43 | } 44 | 45 | return tableSequence.then(function () { 46 | return fixtures.populate(logger, modelOptions); 47 | }); 48 | }; 49 | 50 | module.exports = populate; 51 | -------------------------------------------------------------------------------- /core/server/data/migration/reset.js: -------------------------------------------------------------------------------- 1 | // ### Reset 2 | // Delete all tables from the database in reverse order 3 | var Promise = require('bluebird'), 4 | commands = require('../schema').commands, 5 | schema = require('../schema').tables, 6 | 7 | schemaTables = Object.keys(schema).reverse(), 8 | reset; 9 | 10 | /** 11 | * # Reset 12 | * Deletes all the tables defined in the schema 13 | * Uses reverse order, which ensures that foreign keys are removed before the parent table 14 | * 15 | * @returns {Promise<*>} 16 | */ 17 | reset = function reset() { 18 | return Promise.mapSeries(schemaTables, function (table) { 19 | return commands.deleteTable(table); 20 | }); 21 | }; 22 | 23 | module.exports = reset; 24 | -------------------------------------------------------------------------------- /core/server/data/schema/checks.js: -------------------------------------------------------------------------------- 1 | function isPost(jsonData) { 2 | return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') && 3 | jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug'); 4 | } 5 | 6 | function isTag(jsonData) { 7 | return jsonData.hasOwnProperty('name') && jsonData.hasOwnProperty('slug') && 8 | jsonData.hasOwnProperty('description') && jsonData.hasOwnProperty('parent'); 9 | } 10 | 11 | function isUser(jsonData) { 12 | return jsonData.hasOwnProperty('bio') && jsonData.hasOwnProperty('website') && 13 | jsonData.hasOwnProperty('status') && jsonData.hasOwnProperty('location'); 14 | } 15 | 16 | function isNav(jsonData) { 17 | return jsonData.hasOwnProperty('label') && jsonData.hasOwnProperty('url') && 18 | jsonData.hasOwnProperty('slug') && jsonData.hasOwnProperty('current'); 19 | } 20 | 21 | module.exports = { 22 | isPost: isPost, 23 | isTag: isTag, 24 | isUser: isUser, 25 | isNav: isNav 26 | }; 27 | -------------------------------------------------------------------------------- /core/server/data/schema/clients/index.js: -------------------------------------------------------------------------------- 1 | var sqlite3 = require('./sqlite3'), 2 | mysql = require('./mysql'), 3 | pg = require('./pg'); 4 | 5 | module.exports = { 6 | sqlite3: sqlite3, 7 | mysql: mysql, 8 | pg: pg, 9 | postgres: pg, 10 | postgresql: pg 11 | }; 12 | -------------------------------------------------------------------------------- /core/server/data/schema/clients/mysql.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | db = require('../../../data/db'), 3 | 4 | // private 5 | doRawAndFlatten, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns, 11 | checkPostTable; 12 | 13 | doRawAndFlatten = function doRaw(query, transaction, flattenFn) { 14 | return (transaction || db.knex).raw(query).then(function (response) { 15 | return _.flatten(flattenFn(response)); 16 | }); 17 | }; 18 | 19 | getTables = function getTables(transaction) { 20 | return doRawAndFlatten('show tables', transaction, function (response) { 21 | return _.map(response[0], function (entry) { return _.values(entry); }); 22 | }); 23 | }; 24 | 25 | getIndexes = function getIndexes(table, transaction) { 26 | return doRawAndFlatten('SHOW INDEXES from ' + table, transaction, function (response) { 27 | return _.map(response[0], 'Key_name'); 28 | }); 29 | }; 30 | 31 | getColumns = function getColumns(table, transaction) { 32 | return doRawAndFlatten('SHOW COLUMNS FROM ' + table, transaction, function (response) { 33 | return _.map(response[0], 'Field'); 34 | }); 35 | }; 36 | 37 | // This function changes the type of posts.html and posts.markdown columns to mediumtext. Due to 38 | // a wrong datatype in schema.js some installations using mysql could have been created using the 39 | // data type text instead of mediumtext. 40 | // For details see: https://github.com/TryGhost/Ghost/issues/1947 41 | checkPostTable = function checkPostTable(transaction) { 42 | return (transaction || db.knex).raw('SHOW FIELDS FROM posts where Field ="html" OR Field = "markdown"').then(function (response) { 43 | return _.flatten(_.map(response[0], function (entry) { 44 | if (entry.Type.toLowerCase() !== 'mediumtext') { 45 | return (transaction || db.knex).raw('ALTER TABLE posts MODIFY ' + entry.Field + ' MEDIUMTEXT'); 46 | } 47 | })); 48 | }); 49 | }; 50 | 51 | module.exports = { 52 | checkPostTable: checkPostTable, 53 | getTables: getTables, 54 | getIndexes: getIndexes, 55 | getColumns: getColumns 56 | }; 57 | -------------------------------------------------------------------------------- /core/server/data/schema/clients/pg.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | db = require('../../../data/db'), 3 | 4 | // private 5 | doRawFlattenAndPluck, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns; 11 | 12 | doRawFlattenAndPluck = function doRaw(query, name, transaction) { 13 | return (transaction || db.knex).raw(query).then(function (response) { 14 | return _.flatten(_.map(response.rows, name)); 15 | }); 16 | }; 17 | 18 | getTables = function getTables(transaction) { 19 | return doRawFlattenAndPluck( 20 | 'SELECT table_name FROM information_schema.tables WHERE table_schema = CURRENT_SCHEMA()', 21 | 'table_name', 22 | transaction 23 | ); 24 | }; 25 | 26 | getIndexes = function getIndexes(table, transaction) { 27 | var selectIndexes = 'SELECT t.relname as table_name, i.relname as index_name, a.attname as column_name' + 28 | ' FROM pg_class t, pg_class i, pg_index ix, pg_attribute a' + 29 | ' WHERE t.oid = ix.indrelid and i.oid = ix.indexrelid and' + 30 | ' a.attrelid = t.oid and a.attnum = ANY(ix.indkey) and t.relname = \'' + table + '\''; 31 | 32 | return doRawFlattenAndPluck(selectIndexes, 'index_name', transaction); 33 | }; 34 | 35 | getColumns = function getColumns(table, transaction) { 36 | var selectIndexes = 'SELECT column_name FROM information_schema.columns WHERE table_name = \'' + table + '\''; 37 | 38 | return doRawFlattenAndPluck(selectIndexes, 'column_name', transaction); 39 | }; 40 | 41 | module.exports = { 42 | getTables: getTables, 43 | getIndexes: getIndexes, 44 | getColumns: getColumns 45 | }; 46 | -------------------------------------------------------------------------------- /core/server/data/schema/clients/sqlite3.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | db = require('../../../data/db'), 3 | 4 | // private 5 | doRaw, 6 | 7 | // public 8 | getTables, 9 | getIndexes, 10 | getColumns; 11 | 12 | doRaw = function doRaw(query, transaction, fn) { 13 | if (!fn) { 14 | fn = transaction; 15 | transaction = null; 16 | } 17 | 18 | return (transaction || db.knex).raw(query).then(function (response) { 19 | return fn(response); 20 | }); 21 | }; 22 | 23 | getTables = function getTables(transaction) { 24 | return doRaw('select * from sqlite_master where type = "table"', transaction, function (response) { 25 | return _.reject(_.map(response, 'tbl_name'), function (name) { 26 | return name === 'sqlite_sequence'; 27 | }); 28 | }); 29 | }; 30 | 31 | getIndexes = function getIndexes(table, transaction) { 32 | return doRaw('pragma index_list("' + table + '")', transaction, function (response) { 33 | return _.flatten(_.map(response, 'name')); 34 | }); 35 | }; 36 | 37 | getColumns = function getColumns(table, transaction) { 38 | return doRaw('pragma table_info("' + table + '")', transaction, function (response) { 39 | return _.flatten(_.map(response, 'name')); 40 | }); 41 | }; 42 | 43 | module.exports = { 44 | getTables: getTables, 45 | getIndexes: getIndexes, 46 | getColumns: getColumns 47 | }; 48 | -------------------------------------------------------------------------------- /core/server/data/schema/index.js: -------------------------------------------------------------------------------- 1 | var schema = require('./schema'), 2 | checks = require('./checks'), 3 | commands = require('./commands'), 4 | versioning = require('./versioning'), 5 | defaultSettings = require('./default-settings'); 6 | 7 | module.exports.tables = schema; 8 | module.exports.checks = checks; 9 | module.exports.commands = commands; 10 | module.exports.versioning = versioning; 11 | module.exports.defaultSettings = defaultSettings; 12 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/handler.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | utils = require('../../../utils'), 3 | sitemap = require('./index'); 4 | 5 | // Responsible for handling requests for sitemap files 6 | module.exports = function handler(blogApp) { 7 | var resourceTypes = ['posts', 'authors', 'tags', 'pages'], 8 | verifyResourceType = function verifyResourceType(req, res, next) { 9 | if (!_.includes(resourceTypes, req.params.resource)) { 10 | return res.sendStatus(404); 11 | } 12 | 13 | next(); 14 | }, 15 | getResourceSiteMapXml = function getResourceSiteMapXml(type, page) { 16 | return sitemap.getSiteMapXml(type, page); 17 | }; 18 | 19 | blogApp.get('/sitemap.xml', function sitemapXML(req, res) { 20 | res.set({ 21 | 'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_S, 22 | 'Content-Type': 'text/xml' 23 | }); 24 | res.send(sitemap.getIndexXml()); 25 | }); 26 | 27 | blogApp.get('/sitemap-:resource.xml', verifyResourceType, function sitemapResourceXML(req, res) { 28 | var type = req.params.resource, 29 | page = 1, 30 | siteMapXml = getResourceSiteMapXml(type, page); 31 | 32 | res.set({ 33 | 'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_S, 34 | 'Content-Type': 'text/xml' 35 | }); 36 | res.send(siteMapXml); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/index-generator.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | xml = require('xml'), 3 | moment = require('moment'), 4 | config = require('../../../config'), 5 | utils = require('./utils'), 6 | RESOURCES, 7 | XMLNS_DECLS; 8 | 9 | RESOURCES = ['pages', 'posts', 'authors', 'tags']; 10 | 11 | XMLNS_DECLS = { 12 | _attr: { 13 | xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' 14 | } 15 | }; 16 | 17 | function SiteMapIndexGenerator(opts) { 18 | // Grab the other site map generators from the options 19 | _.extend(this, _.pick(opts, RESOURCES)); 20 | } 21 | 22 | _.extend(SiteMapIndexGenerator.prototype, { 23 | getIndexXml: function () { 24 | var urlElements = this.generateSiteMapUrlElements(), 25 | data = { 26 | // Concat the elements to the _attr declaration 27 | sitemapindex: [XMLNS_DECLS].concat(urlElements) 28 | }; 29 | 30 | // Return the xml 31 | return utils.getDeclarations() + xml(data); 32 | }, 33 | 34 | generateSiteMapUrlElements: function () { 35 | var self = this; 36 | 37 | return _.map(RESOURCES, function (resourceType) { 38 | var url = config.urlFor({ 39 | relativeUrl: '/sitemap-' + resourceType + '.xml' 40 | }, true), 41 | lastModified = self[resourceType].lastModified; 42 | 43 | return { 44 | sitemap: [ 45 | {loc: url}, 46 | {lastmod: moment(lastModified).toISOString()} 47 | ] 48 | }; 49 | }); 50 | } 51 | }); 52 | 53 | module.exports = SiteMapIndexGenerator; 54 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/index.js: -------------------------------------------------------------------------------- 1 | var SiteMapManager = require('./manager'); 2 | 3 | module.exports = new SiteMapManager(); 4 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/page-generator.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../../../api'), 3 | config = require('../../../config'), 4 | BaseMapGenerator = require('./base-generator'); 5 | 6 | // A class responsible for generating a sitemap from posts and keeping it updated 7 | function PageMapGenerator(opts) { 8 | _.extend(this, opts); 9 | 10 | BaseMapGenerator.apply(this, arguments); 11 | } 12 | 13 | // Inherit from the base generator class 14 | _.extend(PageMapGenerator.prototype, BaseMapGenerator.prototype); 15 | 16 | _.extend(PageMapGenerator.prototype, { 17 | bindEvents: function () { 18 | var self = this; 19 | this.dataEvents.on('page.published', self.addOrUpdateUrl.bind(self)); 20 | this.dataEvents.on('page.published.edited', self.addOrUpdateUrl.bind(self)); 21 | this.dataEvents.on('page.unpublished', self.removeUrl.bind(self)); 22 | }, 23 | 24 | getData: function () { 25 | return api.posts.browse({ 26 | context: { 27 | internal: true 28 | }, 29 | status: 'published', 30 | staticPages: true, 31 | limit: 'all' 32 | }).then(function (resp) { 33 | var homePage = { 34 | id: 0, 35 | name: 'home' 36 | }; 37 | return [homePage].concat(resp.posts); 38 | }); 39 | }, 40 | 41 | getUrlForDatum: function (post) { 42 | if (post.id === 0 && !_.isEmpty(post.name)) { 43 | return config.urlFor(post.name, true); 44 | } 45 | 46 | return config.urlFor('post', {post: post}, true); 47 | }, 48 | 49 | getPriorityForDatum: function (post) { 50 | // TODO: We could influence this with priority or meta information 51 | return post && post.name === 'home' ? 1.0 : 0.8; 52 | } 53 | }); 54 | 55 | module.exports = PageMapGenerator; 56 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/post-generator.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../../../api'), 3 | config = require('../../../config'), 4 | BaseMapGenerator = require('./base-generator'); 5 | 6 | // A class responsible for generating a sitemap from posts and keeping it updated 7 | function PostMapGenerator(opts) { 8 | _.extend(this, opts); 9 | 10 | BaseMapGenerator.apply(this, arguments); 11 | } 12 | 13 | // Inherit from the base generator class 14 | _.extend(PostMapGenerator.prototype, BaseMapGenerator.prototype); 15 | 16 | _.extend(PostMapGenerator.prototype, { 17 | bindEvents: function () { 18 | var self = this; 19 | this.dataEvents.on('post.published', self.addOrUpdateUrl.bind(self)); 20 | this.dataEvents.on('post.published.edited', self.addOrUpdateUrl.bind(self)); 21 | this.dataEvents.on('post.unpublished', self.removeUrl.bind(self)); 22 | }, 23 | 24 | getData: function () { 25 | return api.posts.browse({ 26 | context: { 27 | internal: true 28 | }, 29 | status: 'published', 30 | staticPages: false, 31 | limit: 'all' 32 | }).then(function (resp) { 33 | return resp.posts; 34 | }); 35 | }, 36 | 37 | getUrlForDatum: function (post) { 38 | return config.urlFor('post', {post: post}, true); 39 | }, 40 | 41 | getPriorityForDatum: function (post) { 42 | // give a slightly higher priority to featured posts 43 | return post.featured ? 0.9 : 0.8; 44 | } 45 | }); 46 | 47 | module.exports = PostMapGenerator; 48 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/tag-generator.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../../../api'), 3 | config = require('../../../config'), 4 | BaseMapGenerator = require('./base-generator'); 5 | 6 | // A class responsible for generating a sitemap from posts and keeping it updated 7 | function TagsMapGenerator(opts) { 8 | _.extend(this, opts); 9 | 10 | BaseMapGenerator.apply(this, arguments); 11 | } 12 | 13 | // Inherit from the base generator class 14 | _.extend(TagsMapGenerator.prototype, BaseMapGenerator.prototype); 15 | 16 | _.extend(TagsMapGenerator.prototype, { 17 | bindEvents: function () { 18 | var self = this; 19 | this.dataEvents.on('tag.added', self.addOrUpdateUrl.bind(self)); 20 | this.dataEvents.on('tag.edited', self.addOrUpdateUrl.bind(self)); 21 | this.dataEvents.on('tag.deleted', self.removeUrl.bind(self)); 22 | }, 23 | 24 | getData: function () { 25 | return api.tags.browse({ 26 | context: { 27 | internal: true 28 | }, 29 | limit: 'all' 30 | }).then(function (resp) { 31 | return resp.tags; 32 | }); 33 | }, 34 | 35 | getUrlForDatum: function (tag) { 36 | return config.urlFor('tag', {tag: tag}, true); 37 | }, 38 | 39 | getPriorityForDatum: function () { 40 | // TODO: We could influence this with meta information 41 | return 0.6; 42 | } 43 | }); 44 | 45 | module.exports = TagsMapGenerator; 46 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/user-generator.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | api = require('../../../api'), 3 | config = require('../../../config'), 4 | validator = require('validator'), 5 | BaseMapGenerator = require('./base-generator'); 6 | 7 | // A class responsible for generating a sitemap from posts and keeping it updated 8 | function UserMapGenerator(opts) { 9 | _.extend(this, opts); 10 | 11 | BaseMapGenerator.apply(this, arguments); 12 | } 13 | 14 | // Inherit from the base generator class 15 | _.extend(UserMapGenerator.prototype, BaseMapGenerator.prototype); 16 | 17 | _.extend(UserMapGenerator.prototype, { 18 | bindEvents: function () { 19 | var self = this; 20 | this.dataEvents.on('user.activated', self.addOrUpdateUrl.bind(self)); 21 | this.dataEvents.on('user.activated.edited', self.addOrUpdateUrl.bind(self)); 22 | this.dataEvents.on('user.deactivated', self.removeUrl.bind(self)); 23 | }, 24 | 25 | getData: function () { 26 | return api.users.browse({ 27 | context: { 28 | internal: true 29 | }, 30 | limit: 'all' 31 | }).then(function (resp) { 32 | return resp.users; 33 | }); 34 | }, 35 | 36 | getUrlForDatum: function (user) { 37 | return config.urlFor('author', {author: user}, true); 38 | }, 39 | 40 | getPriorityForDatum: function () { 41 | // TODO: We could influence this with meta information 42 | return 0.6; 43 | }, 44 | 45 | validateImageUrl: function (imageUrl) { 46 | return imageUrl && 47 | validator.isURL(imageUrl, {protocols: ['http', 'https'], require_protocol: true}); 48 | } 49 | }); 50 | 51 | module.exports = UserMapGenerator; 52 | -------------------------------------------------------------------------------- /core/server/data/xml/sitemap/utils.js: -------------------------------------------------------------------------------- 1 | var config = require('../../../config'), 2 | utils; 3 | 4 | utils = { 5 | getDeclarations: function () { 6 | var baseUrl = config.urlFor('sitemap_xsl', true); 7 | baseUrl = baseUrl.replace(/^(http:|https:)/, ''); 8 | return '' + 9 | ''; 10 | } 11 | }; 12 | 13 | module.exports = utils; 14 | -------------------------------------------------------------------------------- /core/server/errors/bad-request-error.js: -------------------------------------------------------------------------------- 1 | // # Bad request error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function BadRequestError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 400; 8 | this.errorType = this.name; 9 | } 10 | 11 | BadRequestError.prototype = Object.create(Error.prototype); 12 | BadRequestError.prototype.name = 'BadRequestError'; 13 | 14 | module.exports = BadRequestError; 15 | -------------------------------------------------------------------------------- /core/server/errors/data-import-error.js: -------------------------------------------------------------------------------- 1 | // # Data import error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function DataImportError(message, offendingProperty, value) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 500; 8 | this.errorType = this.name; 9 | this.property = offendingProperty || undefined; 10 | this.value = value || undefined; 11 | } 12 | 13 | DataImportError.prototype = Object.create(Error.prototype); 14 | DataImportError.prototype.name = 'DataImportError'; 15 | 16 | module.exports = DataImportError; 17 | -------------------------------------------------------------------------------- /core/server/errors/database-not-populated.js: -------------------------------------------------------------------------------- 1 | function DatabaseNotPopulated(message) { 2 | this.message = message; 3 | this.stack = new Error().stack; 4 | this.statusCode = 500; 5 | this.errorType = this.name; 6 | } 7 | 8 | DatabaseNotPopulated.prototype = Object.create(Error.prototype); 9 | DatabaseNotPopulated.prototype.name = 'DatabaseNotPopulated'; 10 | 11 | module.exports = DatabaseNotPopulated; 12 | -------------------------------------------------------------------------------- /core/server/errors/database-version.js: -------------------------------------------------------------------------------- 1 | function DatabaseVersion(message, context, help) { 2 | this.message = message; 3 | this.stack = new Error().stack; 4 | this.statusCode = 500; 5 | this.errorType = this.name; 6 | this.context = context; 7 | this.help = help; 8 | } 9 | 10 | DatabaseVersion.prototype = Object.create(Error.prototype); 11 | DatabaseVersion.prototype.name = 'DatabaseVersion'; 12 | 13 | module.exports = DatabaseVersion; 14 | -------------------------------------------------------------------------------- /core/server/errors/email-error.js: -------------------------------------------------------------------------------- 1 | // # Email error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function EmailError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 500; 8 | this.errorType = this.name; 9 | } 10 | 11 | EmailError.prototype = Object.create(Error.prototype); 12 | EmailError.prototype.name = 'EmailError'; 13 | 14 | module.exports = EmailError; 15 | -------------------------------------------------------------------------------- /core/server/errors/incorrect-usage.js: -------------------------------------------------------------------------------- 1 | function IncorrectUsage(message, context) { 2 | this.name = 'IncorrectUsage'; 3 | this.stack = new Error().stack; 4 | this.statusCode = 400; 5 | this.errorType = this.name; 6 | this.message = message; 7 | this.context = context; 8 | } 9 | 10 | IncorrectUsage.prototype = Object.create(Error.prototype); 11 | module.exports = IncorrectUsage; 12 | -------------------------------------------------------------------------------- /core/server/errors/internal-server-error.js: -------------------------------------------------------------------------------- 1 | // # Internal Server Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function InternalServerError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 500; 8 | this.errorType = this.name; 9 | } 10 | 11 | InternalServerError.prototype = Object.create(Error.prototype); 12 | InternalServerError.prototype.name = 'InternalServerError'; 13 | 14 | module.exports = InternalServerError; 15 | -------------------------------------------------------------------------------- /core/server/errors/maintenance.js: -------------------------------------------------------------------------------- 1 | function Maintenance(message) { 2 | this.message = message; 3 | this.stack = new Error().stack; 4 | this.statusCode = 503; 5 | this.errorType = this.name; 6 | } 7 | 8 | Maintenance.prototype = Object.create(Error.prototype); 9 | Maintenance.prototype.name = 'Maintenance'; 10 | 11 | module.exports = Maintenance; 12 | -------------------------------------------------------------------------------- /core/server/errors/method-not-allowed-error.js: -------------------------------------------------------------------------------- 1 | // # Not found error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function MethodNotAllowedError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 405; 8 | this.errorType = this.name; 9 | } 10 | 11 | MethodNotAllowedError.prototype = Object.create(Error.prototype); 12 | MethodNotAllowedError.prototype.name = 'MethodNotAllowedError'; 13 | 14 | module.exports = MethodNotAllowedError; 15 | -------------------------------------------------------------------------------- /core/server/errors/no-permission-error.js: -------------------------------------------------------------------------------- 1 | // # No Permission Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function NoPermissionError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 403; 8 | this.errorType = this.name; 9 | } 10 | 11 | NoPermissionError.prototype = Object.create(Error.prototype); 12 | NoPermissionError.prototype.name = 'NoPermissionError'; 13 | 14 | module.exports = NoPermissionError; 15 | -------------------------------------------------------------------------------- /core/server/errors/not-found-error.js: -------------------------------------------------------------------------------- 1 | // # Not found error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function NotFoundError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 404; 8 | this.errorType = this.name; 9 | } 10 | 11 | NotFoundError.prototype = Object.create(Error.prototype); 12 | NotFoundError.prototype.name = 'NotFoundError'; 13 | 14 | module.exports = NotFoundError; 15 | -------------------------------------------------------------------------------- /core/server/errors/request-too-large-error.js: -------------------------------------------------------------------------------- 1 | // # Request Entity Too Large Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function RequestEntityTooLargeError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 413; 8 | this.errorType = this.name; 9 | } 10 | 11 | RequestEntityTooLargeError.prototype = Object.create(Error.prototype); 12 | RequestEntityTooLargeError.prototype.name = 'RequestEntityTooLargeError'; 13 | 14 | module.exports = RequestEntityTooLargeError; 15 | -------------------------------------------------------------------------------- /core/server/errors/token-revocation-error.js: -------------------------------------------------------------------------------- 1 | // # Token Revocation ERror 2 | // Custom error class with status code and type prefilled. 3 | 4 | function TokenRevocationError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 503; 8 | this.errorType = this.name; 9 | } 10 | 11 | TokenRevocationError.prototype = Object.create(Error.prototype); 12 | TokenRevocationError.prototype.name = 'TokenRevocationError'; 13 | 14 | module.exports = TokenRevocationError; 15 | -------------------------------------------------------------------------------- /core/server/errors/too-many-requests-error.js: -------------------------------------------------------------------------------- 1 | // # Too Many Requests Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function TooManyRequestsError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 429; 8 | this.errorType = this.name; 9 | } 10 | 11 | TooManyRequestsError.prototype = Object.create(Error.prototype); 12 | TooManyRequestsError.prototype.name = 'TooManyRequestsError'; 13 | 14 | module.exports = TooManyRequestsError; 15 | -------------------------------------------------------------------------------- /core/server/errors/unauthorized-error.js: -------------------------------------------------------------------------------- 1 | // # Unauthorized error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function UnauthorizedError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 401; 8 | this.errorType = this.name; 9 | } 10 | 11 | UnauthorizedError.prototype = Object.create(Error.prototype); 12 | UnauthorizedError.prototype.name = 'UnauthorizedError'; 13 | 14 | module.exports = UnauthorizedError; 15 | -------------------------------------------------------------------------------- /core/server/errors/unsupported-media-type-error.js: -------------------------------------------------------------------------------- 1 | // # Unsupported Media Type 2 | // Custom error class with status code and type prefilled. 3 | 4 | function UnsupportedMediaTypeError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 415; 8 | this.errorType = this.name; 9 | } 10 | 11 | UnsupportedMediaTypeError.prototype = Object.create(Error.prototype); 12 | UnsupportedMediaTypeError.prototype.name = 'UnsupportedMediaTypeError'; 13 | 14 | module.exports = UnsupportedMediaTypeError; 15 | -------------------------------------------------------------------------------- /core/server/errors/validation-error.js: -------------------------------------------------------------------------------- 1 | // # Validation Error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function ValidationError(message, offendingProperty) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 422; 8 | if (offendingProperty) { 9 | this.property = offendingProperty; 10 | } 11 | this.errorType = this.name; 12 | } 13 | 14 | ValidationError.prototype = Object.create(Error.prototype); 15 | ValidationError.prototype.name = 'ValidationError'; 16 | 17 | module.exports = ValidationError; 18 | -------------------------------------------------------------------------------- /core/server/errors/version-mismatch-error.js: -------------------------------------------------------------------------------- 1 | // # Version mismatch error 2 | // Custom error class with status code and type prefilled. 3 | 4 | function VersionMismatchError(message) { 5 | this.message = message; 6 | this.stack = new Error().stack; 7 | this.statusCode = 400; 8 | this.errorType = this.name; 9 | } 10 | 11 | VersionMismatchError.prototype = Object.create(Error.prototype); 12 | VersionMismatchError.prototype.name = 'VersionMismatchError'; 13 | 14 | module.exports = VersionMismatchError; 15 | -------------------------------------------------------------------------------- /core/server/events/index.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'), 3 | EventRegistry, 4 | EventRegistryInstance; 5 | 6 | EventRegistry = function () { 7 | events.EventEmitter.call(this); 8 | }; 9 | 10 | util.inherits(EventRegistry, events.EventEmitter); 11 | 12 | EventRegistry.prototype.onMany = function (arr, onEvent) { 13 | var self = this; 14 | 15 | arr.forEach(function (eventName) { 16 | self.on(eventName, onEvent); 17 | }); 18 | }; 19 | 20 | EventRegistryInstance = new EventRegistry(); 21 | EventRegistryInstance.setMaxListeners(100); 22 | 23 | module.exports = EventRegistryInstance; 24 | -------------------------------------------------------------------------------- /core/server/helpers/asset.js: -------------------------------------------------------------------------------- 1 | // # Asset helper 2 | // Usage: `{{asset "css/screen.css"}}`, `{{asset "css/screen.css" ghost="true"}}` 3 | // 4 | // Returns the path to the specified asset. The ghost flag outputs the asset path for the Ghost admin 5 | 6 | var getAssetUrl = require('../data/meta/asset_url'), 7 | hbs = require('express-hbs'); 8 | 9 | function asset(path, options) { 10 | var isAdmin, 11 | minify; 12 | 13 | if (options && options.hash) { 14 | isAdmin = options.hash.ghost; 15 | minify = options.hash.minifyInProduction; 16 | } 17 | if (process.env.NODE_ENV !== 'production') { 18 | minify = false; 19 | } 20 | return new hbs.handlebars.SafeString( 21 | getAssetUrl(path, isAdmin, minify) 22 | ); 23 | } 24 | 25 | module.exports = asset; 26 | -------------------------------------------------------------------------------- /core/server/helpers/author.js: -------------------------------------------------------------------------------- 1 | // # Author Helper 2 | // Usage: `{{author}}` OR `{{#author}}{{/author}}` 3 | // 4 | // Can be used as either an output or a block helper 5 | // 6 | // Output helper: `{{author}}` 7 | // Returns the full name of the author of a given post, or a blank string 8 | // if the author could not be determined. 9 | // 10 | // Block helper: `{{#author}}{{/author}}` 11 | // This is the default handlebars behaviour of dropping into the author object scope 12 | 13 | var hbs = require('express-hbs'), 14 | _ = require('lodash'), 15 | config = require('../config'), 16 | utils = require('./utils'), 17 | author; 18 | 19 | author = function (options) { 20 | if (options.fn) { 21 | return hbs.handlebars.helpers.with.call(this, this.author, options); 22 | } 23 | 24 | var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, 25 | output = ''; 26 | 27 | if (this.author && this.author.name) { 28 | if (autolink) { 29 | output = utils.linkTemplate({ 30 | url: config.urlFor('author', {author: this.author}), 31 | text: _.escape(this.author.name) 32 | }); 33 | } else { 34 | output = _.escape(this.author.name); 35 | } 36 | } 37 | 38 | return new hbs.handlebars.SafeString(output); 39 | }; 40 | 41 | module.exports = author; 42 | -------------------------------------------------------------------------------- /core/server/helpers/content.js: -------------------------------------------------------------------------------- 1 | // # Content Helper 2 | // Usage: `{{content}}`, `{{content words="20"}}`, `{{content characters="256"}}` 3 | // 4 | // Turns content html into a safestring so that the user doesn't have to 5 | // escape it or tell handlebars to leave it alone with a triple-brace. 6 | // 7 | // Enables tag-safe truncation of content by characters or words. 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | downsize = require('downsize'), 12 | downzero = require('../utils/downzero'), 13 | content; 14 | 15 | content = function (options) { 16 | var truncateOptions = (options || {}).hash || {}; 17 | truncateOptions = _.pick(truncateOptions, ['words', 'characters']); 18 | _.keys(truncateOptions).map(function (key) { 19 | truncateOptions[key] = parseInt(truncateOptions[key], 10); 20 | }); 21 | 22 | if (truncateOptions.hasOwnProperty('words') || truncateOptions.hasOwnProperty('characters')) { 23 | // Legacy function: {{content words="0"}} should return leading tags. 24 | if (truncateOptions.hasOwnProperty('words') && truncateOptions.words === 0) { 25 | return new hbs.handlebars.SafeString( 26 | downzero(this.html) 27 | ); 28 | } 29 | 30 | return new hbs.handlebars.SafeString( 31 | downsize(this.html, truncateOptions) 32 | ); 33 | } 34 | 35 | return new hbs.handlebars.SafeString(this.html); 36 | }; 37 | 38 | module.exports = content; 39 | -------------------------------------------------------------------------------- /core/server/helpers/date.js: -------------------------------------------------------------------------------- 1 | // # Date Helper 2 | // Usage: `{{date format="DD MM, YYYY"}}`, `{{date updated_at format="DD MM, YYYY"}}` 3 | // 4 | // Formats a date using moment-timezone.js. Formats published_at by default but will also take a date as a parameter 5 | 6 | var moment = require('moment-timezone'), 7 | date, 8 | timezone; 9 | 10 | date = function (date, options) { 11 | if (!options && date.hasOwnProperty('hash')) { 12 | options = date; 13 | date = undefined; 14 | timezone = options.data.blog.timezone; 15 | 16 | // set to published_at by default, if it's available 17 | // otherwise, this will print the current date 18 | if (this.published_at) { 19 | date = moment(this.published_at).tz(timezone).format(); 20 | } 21 | } 22 | 23 | // ensure that context is undefined, not null, as that can cause errors 24 | date = date === null ? undefined : date; 25 | 26 | var f = options.hash.format || 'MMM DD, YYYY', 27 | timeago = options.hash.timeago, 28 | timeNow = moment().tz(timezone); 29 | 30 | if (timeago) { 31 | date = timezone ? moment(date).tz(timezone).from(timeNow) : moment(date).fromNow(); 32 | } else { 33 | date = timezone ? moment(date).tz(timezone).format(f) : moment(date).format(f); 34 | } 35 | 36 | return date; 37 | }; 38 | 39 | module.exports = date; 40 | -------------------------------------------------------------------------------- /core/server/helpers/encode.js: -------------------------------------------------------------------------------- 1 | // # Encode Helper 2 | // 3 | // Usage: `{{encode uri}}` 4 | // 5 | // Returns URI encoded string 6 | 7 | var hbs = require('express-hbs'), 8 | encode; 9 | 10 | encode = function (string, options) { 11 | var uri = string || options; 12 | return new hbs.handlebars.SafeString(encodeURIComponent(uri)); 13 | }; 14 | 15 | module.exports = encode; 16 | -------------------------------------------------------------------------------- /core/server/helpers/excerpt.js: -------------------------------------------------------------------------------- 1 | // # Excerpt Helper 2 | // Usage: `{{excerpt}}`, `{{excerpt words="50"}}`, `{{excerpt characters="256"}}` 3 | // 4 | // Attempts to remove all HTML from the string, and then shortens the result according to the provided option. 5 | // 6 | // Defaults to words="50" 7 | 8 | var hbs = require('express-hbs'), 9 | _ = require('lodash'), 10 | getMetaDataExcerpt = require('../data/meta/excerpt'); 11 | 12 | function excerpt(options) { 13 | var truncateOptions = (options || {}).hash || {}; 14 | 15 | truncateOptions = _.pick(truncateOptions, ['words', 'characters']); 16 | _.keys(truncateOptions).map(function (key) { 17 | truncateOptions[key] = parseInt(truncateOptions[key], 10); 18 | }); 19 | 20 | return new hbs.handlebars.SafeString( 21 | getMetaDataExcerpt(String(this.html), truncateOptions) 22 | ); 23 | } 24 | 25 | module.exports = excerpt; 26 | -------------------------------------------------------------------------------- /core/server/helpers/facebook_url.js: -------------------------------------------------------------------------------- 1 | // # Facebook URL Helper 2 | // Usage: `{{facebook_url}}` or `{{facebook_url author.facebook}}` 3 | // 4 | // Output a url for a twitter username 5 | // 6 | // We use the name facebook_url to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var socialUrls = require('../utils/social-urls'), 10 | findKey = require('./utils').findKey, 11 | facebook_url; 12 | 13 | facebook_url = function (username, options) { 14 | if (!options) { 15 | options = username; 16 | username = findKey('facebook', this, options.data.blog); 17 | } 18 | 19 | if (username) { 20 | return socialUrls.facebookUrl(username); 21 | } 22 | 23 | return null; 24 | }; 25 | 26 | module.exports = facebook_url; 27 | -------------------------------------------------------------------------------- /core/server/helpers/ghost_foot.js: -------------------------------------------------------------------------------- 1 | // # Ghost Foot Helper 2 | // Usage: `{{ghost_foot}}` 3 | // 4 | // Outputs scripts and other assets at the bottom of a Ghost theme 5 | // 6 | // We use the name ghost_foot to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | filters = require('../filters'), 12 | api = require('../api'), 13 | ghost_foot; 14 | 15 | ghost_foot = function (options) { 16 | /*jshint unused:false*/ 17 | var foot = []; 18 | 19 | return api.settings.read({key: 'ghost_foot'}).then(function (response) { 20 | foot.push(response.settings[0].value); 21 | return filters.doFilter('ghost_foot', foot); 22 | }).then(function (foot) { 23 | var footString = _.reduce(foot, function (memo, item) { return memo + ' ' + item; }, ''); 24 | return new hbs.handlebars.SafeString(footString.trim()); 25 | }); 26 | }; 27 | 28 | module.exports = ghost_foot; 29 | -------------------------------------------------------------------------------- /core/server/helpers/has.js: -------------------------------------------------------------------------------- 1 | // # Has Helper 2 | // Usage: `{{#has tag="video, music"}}`, `{{#has author="sam, pat"}}` 3 | // 4 | // Checks if a post has a particular property 5 | 6 | var _ = require('lodash'), 7 | errors = require('../errors'), 8 | i18n = require('../i18n'), 9 | has; 10 | 11 | has = function (options) { 12 | options = options || {}; 13 | options.hash = options.hash || {}; 14 | 15 | var tags = _.map(this.tags, 'name'), 16 | author = this.author ? this.author.name : null, 17 | tagList = options.hash.tag || false, 18 | authorList = options.hash.author || false, 19 | tagsOk, 20 | authorOk; 21 | 22 | function evaluateTagList(expr, tags) { 23 | return expr.split(',').map(function (v) { 24 | return v.trim(); 25 | }).reduce(function (p, c) { 26 | return p || (_.findIndex(tags, function (item) { 27 | // Escape regex special characters 28 | item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); 29 | item = new RegExp('^' + item + '$', 'i'); 30 | return item.test(c); 31 | }) !== -1); 32 | }, false); 33 | } 34 | 35 | function evaluateAuthorList(expr, author) { 36 | var authorList = expr.split(',').map(function (v) { 37 | return v.trim().toLocaleLowerCase(); 38 | }); 39 | 40 | return _.includes(authorList, author.toLocaleLowerCase()); 41 | } 42 | 43 | if (!tagList && !authorList) { 44 | errors.logWarn(i18n.t('warnings.helpers.has.invalidAttribute')); 45 | return; 46 | } 47 | 48 | tagsOk = tagList && evaluateTagList(tagList, tags) || false; 49 | authorOk = authorList && evaluateAuthorList(authorList, author) || false; 50 | 51 | if (tagsOk || authorOk) { 52 | return options.fn(this); 53 | } 54 | return options.inverse(this); 55 | }; 56 | 57 | module.exports = has; 58 | -------------------------------------------------------------------------------- /core/server/helpers/image.js: -------------------------------------------------------------------------------- 1 | 2 | // Usage: `{{image}}`, `{{image absolute="true"}}` 3 | // 4 | // Returns the URL for the current object scope i.e. If inside a post scope will return image permalink 5 | // `absolute` flag outputs absolute URL, else URL is relative. 6 | 7 | var config = require('../config'), 8 | image; 9 | 10 | image = function (options) { 11 | var absolute = options && options.hash.absolute; 12 | 13 | if (this.image) { 14 | return config.urlFor('image', {image: this.image}, absolute); 15 | } 16 | }; 17 | 18 | module.exports = image; 19 | -------------------------------------------------------------------------------- /core/server/helpers/input_email.js: -------------------------------------------------------------------------------- 1 | // # Input Email Helper 2 | // Usage: `{{input_email}}` 3 | // 4 | // Password input used on private.hbs for password-protected blogs 5 | // 6 | // We use the name meta_title to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | utils = require('./utils'), 11 | input_email; 12 | 13 | input_email = function (options) { 14 | options = options || {}; 15 | options.hash = options.hash || {}; 16 | 17 | var className = (options.hash.class) ? options.hash.class : 'subscribe-email', 18 | extras = '', 19 | output; 20 | 21 | if (options.hash.autofocus) { 22 | extras += 'autofocus="autofocus"'; 23 | } 24 | 25 | if (options.hash.placeholder) { 26 | extras += ' placeholder="' + options.hash.placeholder + '"'; 27 | } 28 | 29 | if (options.hash.value) { 30 | extras += ' value="' + options.hash.value + '"'; 31 | } 32 | 33 | output = utils.inputTemplate({ 34 | type: 'email', 35 | name: 'email', 36 | className: className, 37 | extras: extras 38 | }); 39 | 40 | return new hbs.handlebars.SafeString(output); 41 | }; 42 | 43 | module.exports = input_email; 44 | -------------------------------------------------------------------------------- /core/server/helpers/input_password.js: -------------------------------------------------------------------------------- 1 | // # Input Password Helper 2 | // Usage: `{{input_password}}` 3 | // 4 | // Password input used on private.hbs for password-protected blogs 5 | // 6 | // We use the name meta_title to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | utils = require('./utils'), 11 | input_password; 12 | 13 | input_password = function (options) { 14 | options = options || {}; 15 | options.hash = options.hash || {}; 16 | 17 | var className = (options.hash.class) ? options.hash.class : 'private-login-password', 18 | extras = 'autofocus="autofocus"', 19 | output; 20 | 21 | if (options.hash.placeholder) { 22 | extras += ' placeholder="' + options.hash.placeholder + '"'; 23 | } 24 | 25 | output = utils.inputTemplate({ 26 | type: 'password', 27 | name: 'password', 28 | className: className, 29 | extras: extras 30 | }); 31 | 32 | return new hbs.handlebars.SafeString(output); 33 | }; 34 | 35 | module.exports = input_password; 36 | -------------------------------------------------------------------------------- /core/server/helpers/is.js: -------------------------------------------------------------------------------- 1 | // # Is Helper 2 | // Usage: `{{#is "paged"}}`, `{{#is "index, paged"}}` 3 | // Checks whether we're in a given context. 4 | var _ = require('lodash'), 5 | errors = require('../errors'), 6 | i18n = require('../i18n'), 7 | is; 8 | 9 | is = function (context, options) { 10 | options = options || {}; 11 | 12 | var currentContext = options.data.root.context; 13 | 14 | if (!_.isString(context)) { 15 | errors.logWarn(i18n.t('warnings.helpers.is.invalidAttribute')); 16 | return; 17 | } 18 | 19 | function evaluateContext(expr) { 20 | return expr.split(',').map(function (v) { 21 | return v.trim(); 22 | }).reduce(function (p, c) { 23 | return p || _.includes(currentContext, c); 24 | }, false); 25 | } 26 | 27 | if (evaluateContext(context)) { 28 | return options.fn(this); 29 | } 30 | return options.inverse(this); 31 | }; 32 | 33 | module.exports = is; 34 | -------------------------------------------------------------------------------- /core/server/helpers/meta_description.js: -------------------------------------------------------------------------------- 1 | // # Meta Description Helper 2 | // Usage: `{{meta_description}}` 3 | // 4 | // Page description used for sharing and SEO 5 | // 6 | // We use the name meta_description to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var getMetaDataDescription = require('../data/meta/description'); 10 | 11 | function meta_description(options) { 12 | options = options || {}; 13 | 14 | return getMetaDataDescription(this, options.data.root) || ''; 15 | } 16 | 17 | module.exports = meta_description; 18 | -------------------------------------------------------------------------------- /core/server/helpers/meta_title.js: -------------------------------------------------------------------------------- 1 | // # Meta Title Helper 2 | // Usage: `{{meta_title}}` 3 | // 4 | // Page title used for sharing and SEO 5 | // 6 | // We use the name meta_title to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var getMetaDataTitle = require('../data/meta/title'); 10 | 11 | function meta_title(options) { 12 | return getMetaDataTitle(this, options.data.root); 13 | } 14 | 15 | module.exports = meta_title; 16 | -------------------------------------------------------------------------------- /core/server/helpers/page_url.js: -------------------------------------------------------------------------------- 1 | // ### Page URL Helper 2 | // 3 | // *Usage example:* 4 | // `{{page_url 2}}` 5 | // 6 | // Returns the URL for the page specified in the current object 7 | // context. 8 | // 9 | // We use the name page_url to match the helper for consistency: 10 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 11 | var errors = require('../errors'), 12 | i18n = require('../i18n'), 13 | getPaginatedUrl = require('../data/meta/paginated_url'), 14 | page_url, 15 | pageUrl; 16 | 17 | page_url = function (page, options) { 18 | if (!options) { 19 | options = page; 20 | page = 1; 21 | } 22 | return getPaginatedUrl(page, options.data.root); 23 | }; 24 | 25 | // ### Page URL Helper: DEPRECATED 26 | // 27 | // *Usage example:* 28 | // `{{pageUrl 2}}` 29 | // 30 | // Returns the URL for the page specified in the current object 31 | // context. This helper is deprecated and will be removed in future versions. 32 | // 33 | pageUrl = function (pageNum, options) { 34 | errors.logWarn(i18n.t('warnings.helpers.page_url.isDeprecated')); 35 | 36 | /*jshint unused:false*/ 37 | var self = this; 38 | 39 | return page_url.call(self, pageNum, options); 40 | }; 41 | 42 | module.exports = page_url; 43 | module.exports.deprecated = pageUrl; 44 | -------------------------------------------------------------------------------- /core/server/helpers/pagination.js: -------------------------------------------------------------------------------- 1 | // ### Pagination Helper 2 | // `{{pagination}}` 3 | // Outputs previous and next buttons, along with info about the current page 4 | 5 | var _ = require('lodash'), 6 | errors = require('../errors'), 7 | template = require('./template'), 8 | i18n = require('../i18n'), 9 | pagination; 10 | 11 | pagination = function (options) { 12 | /*jshint unused:false*/ 13 | if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { 14 | return errors.logAndThrowError(i18n.t('warnings.helpers.pagination.invalidData')); 15 | } 16 | 17 | if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || 18 | _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { 19 | return errors.logAndThrowError(i18n.t('warnings.helpers.pagination.valuesMustBeDefined')); 20 | } 21 | 22 | if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) || 23 | (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { 24 | return errors.logAndThrowError(i18n.t('warnings.helpers.pagination.nextPrevValuesMustBeNumeric')); 25 | } 26 | 27 | if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || 28 | !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { 29 | return errors.logAndThrowError(i18n.t('warnings.helpers.pagination.valuesMustBeNumeric')); 30 | } 31 | 32 | var data = _.merge({}, this.pagination); 33 | 34 | return template.execute('pagination', data, options); 35 | }; 36 | 37 | module.exports = pagination; 38 | -------------------------------------------------------------------------------- /core/server/helpers/plural.js: -------------------------------------------------------------------------------- 1 | // # Plural Helper 2 | // Usage: `{{plural 0 empty='No posts' singular='% post' plural='% posts'}}` 3 | // 4 | // pluralises strings depending on item count 5 | // 6 | // The 1st argument is the numeric variable which the helper operates on 7 | // The 2nd argument is the string that will be output if the variable's value is 0 8 | // The 3rd argument is the string that will be output if the variable's value is 1 9 | // The 4th argument is the string that will be output if the variable's value is 2+ 10 | 11 | var hbs = require('express-hbs'), 12 | errors = require('../errors'), 13 | _ = require('lodash'), 14 | i18n = require('../i18n'), 15 | plural; 16 | 17 | plural = function (number, options) { 18 | if (_.isUndefined(options.hash) || _.isUndefined(options.hash.empty) || 19 | _.isUndefined(options.hash.singular) || _.isUndefined(options.hash.plural)) { 20 | return errors.logAndThrowError(i18n.t('warnings.helpers.plural.valuesMustBeDefined')); 21 | } 22 | 23 | if (number === 0) { 24 | return new hbs.handlebars.SafeString(options.hash.empty.replace('%', number)); 25 | } else if (number === 1) { 26 | return new hbs.handlebars.SafeString(options.hash.singular.replace('%', number)); 27 | } else if (number >= 2) { 28 | return new hbs.handlebars.SafeString(options.hash.plural.replace('%', number)); 29 | } 30 | }; 31 | 32 | module.exports = plural; 33 | -------------------------------------------------------------------------------- /core/server/helpers/post_class.js: -------------------------------------------------------------------------------- 1 | // # Post Class Helper 2 | // Usage: `{{post_class}}` 3 | // 4 | // Output classes for the body element 5 | // 6 | // We use the name body_class to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var hbs = require('express-hbs'), 10 | _ = require('lodash'), 11 | post_class; 12 | 13 | post_class = function (options) { 14 | /*jshint unused:false*/ 15 | var classes = ['post'], 16 | tags = this.post && this.post.tags ? this.post.tags : this.tags || [], 17 | featured = this.post && this.post.featured ? this.post.featured : this.featured || false, 18 | page = this.post && this.post.page ? this.post.page : this.page || false; 19 | 20 | if (tags) { 21 | classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); 22 | } 23 | 24 | if (featured) { 25 | classes.push('featured'); 26 | } 27 | 28 | if (page) { 29 | classes.push('page'); 30 | } 31 | 32 | classes = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); 33 | return new hbs.handlebars.SafeString(classes.trim()); 34 | }; 35 | 36 | module.exports = post_class; 37 | -------------------------------------------------------------------------------- /core/server/helpers/prev_next.js: -------------------------------------------------------------------------------- 1 | // ### prevNext helper exposes methods for prev_post and next_post - separately defined in helpers index. 2 | // Example usages 3 | // `{{#prev_post}}next post{{/next_post}}' 5 | 6 | var api = require('../api'), 7 | schema = require('../data/schema').checks, 8 | Promise = require('bluebird'), 9 | fetch, prevNext; 10 | 11 | fetch = function (apiOptions, options) { 12 | return api.posts.read(apiOptions).then(function (result) { 13 | var related = result.posts[0]; 14 | 15 | if (related.previous) { 16 | return options.fn(related.previous); 17 | } else if (related.next) { 18 | return options.fn(related.next); 19 | } else { 20 | return options.inverse(this); 21 | } 22 | }); 23 | }; 24 | 25 | // If prevNext method is called without valid post data then we must return a promise, if there is valid post data 26 | // then the promise is handled in the api call. 27 | 28 | prevNext = function (options) { 29 | options = options || {}; 30 | 31 | var apiOptions = { 32 | include: options.name === 'prev_post' ? 'previous,previous.author,previous.tags' : 'next,next.author,next.tags' 33 | }; 34 | 35 | if (schema.isPost(this) && this.status === 'published') { 36 | apiOptions.slug = this.slug; 37 | return fetch(apiOptions, options); 38 | } else { 39 | return Promise.resolve(options.inverse(this)); 40 | } 41 | }; 42 | 43 | module.exports = prevNext; 44 | -------------------------------------------------------------------------------- /core/server/helpers/template.js: -------------------------------------------------------------------------------- 1 | var templates = {}, 2 | hbs = require('express-hbs'), 3 | errors = require('../errors'), 4 | i18n = require('../i18n'); 5 | 6 | // ## Template utils 7 | 8 | // Execute a template helper 9 | // All template helpers are register as partial view. 10 | templates.execute = function (name, context, options) { 11 | var partial = hbs.handlebars.partials[name]; 12 | 13 | if (partial === undefined) { 14 | errors.logAndThrowError(i18n.t('warnings.helpers.template.templateNotFound', {name: name})); 15 | return; 16 | } 17 | 18 | // If the partial view is not compiled, it compiles and saves in handlebars 19 | if (typeof partial === 'string') { 20 | hbs.registerPartial(partial); 21 | } 22 | 23 | return new hbs.handlebars.SafeString(partial(context, options)); 24 | }; 25 | 26 | module.exports = templates; 27 | -------------------------------------------------------------------------------- /core/server/helpers/title.js: -------------------------------------------------------------------------------- 1 | // # Title Helper 2 | // Usage: `{{title}}` 3 | // 4 | // Overrides the standard behaviour of `{[title}}` to ensure the content is correctly escaped 5 | 6 | var hbs = require('express-hbs'), 7 | title; 8 | 9 | title = function () { 10 | return new hbs.handlebars.SafeString(hbs.handlebars.Utils.escapeExpression(this.title || '')); 11 | }; 12 | 13 | module.exports = title; 14 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/navigation.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/pagination.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /core/server/helpers/tpl/subscribe_form.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{! This is required for the form to work correctly }} 3 | {{hidden}} 4 | 5 |
    6 | {{input_email class=input_class placeholder=placeholder value=email autofocus=autofocus}} 7 |
    8 | 9 | {{! This is used to get extra info about where this subscriber came from }} 10 | {{script}} 11 |
    12 | 13 | {{#if error}} 14 |

    {{{error.message}}}

    15 | {{/if}} 16 | -------------------------------------------------------------------------------- /core/server/helpers/twitter_url.js: -------------------------------------------------------------------------------- 1 | // # Twitter URL Helper 2 | // Usage: `{{twitter_url}}` or `{{twitter_url author.twitter}}` 3 | // 4 | // Output a url for a twitter username 5 | // 6 | // We use the name twitter_url to match the helper for consistency: 7 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 8 | 9 | var socialUrls = require('../utils/social-urls'), 10 | findKey = require('./utils').findKey, 11 | twitter_url; 12 | 13 | twitter_url = function twitter_url(username, options) { 14 | if (!options) { 15 | options = username; 16 | username = findKey('twitter', this, options.data.blog); 17 | } 18 | 19 | if (username) { 20 | return socialUrls.twitterUrl(username); 21 | } 22 | 23 | return null; 24 | }; 25 | 26 | module.exports = twitter_url; 27 | -------------------------------------------------------------------------------- /core/server/helpers/url.js: -------------------------------------------------------------------------------- 1 | // # URL helper 2 | // Usage: `{{url}}`, `{{url absolute="true"}}` 3 | // 4 | // Returns the URL for the current object scope i.e. If inside a post scope will return post permalink 5 | // `absolute` flag outputs absolute URL, else URL is relative 6 | 7 | var getMetaDataUrl = require('../data/meta/url'); 8 | 9 | function url(options) { 10 | var absolute = options && options.hash.absolute; 11 | 12 | return getMetaDataUrl(this, absolute); 13 | } 14 | 15 | module.exports = url; 16 | -------------------------------------------------------------------------------- /core/server/helpers/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | utils; 3 | 4 | utils = { 5 | assetTemplate: _.template('<%= source %>?v=<%= version %>'), 6 | linkTemplate: _.template('<%= text %>'), 7 | scriptTemplate: _.template(''), 8 | inputTemplate: _.template(' />'), 9 | isProduction: process.env.NODE_ENV === 'production', 10 | // @TODO this can probably be made more generic and used in more places 11 | findKey: function findKey(key, object, data) { 12 | if (object && _.has(object, key) && !_.isEmpty(object[key])) { 13 | return object[key]; 14 | } 15 | 16 | if (data && _.has(data, key) && !_.isEmpty(data[key])) { 17 | return data[key]; 18 | } 19 | 20 | return null; 21 | }, 22 | parseVisibility: function parseVisibility(options) { 23 | if (!options.hash.visibility) { 24 | return ['public']; 25 | } 26 | 27 | return _.map(options.hash.visibility.split(','), _.trim); 28 | } 29 | }; 30 | 31 | module.exports = utils; 32 | -------------------------------------------------------------------------------- /core/server/mail/index.js: -------------------------------------------------------------------------------- 1 | exports.GhostMailer = require('./GhostMailer'); 2 | exports.utils = require('./utils'); 3 | -------------------------------------------------------------------------------- /core/server/mail/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash').runInContext(), 2 | fs = require('fs'), 3 | Promise = require('bluebird'), 4 | path = require('path'), 5 | htmlToText = require('html-to-text'), 6 | config = require('../config'), 7 | templatesDir = path.resolve(__dirname, '..', 'mail', 'templates'); 8 | 9 | _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; 10 | 11 | exports.generateContent = function generateContent(options) { 12 | var defaults, 13 | data; 14 | 15 | defaults = { 16 | siteUrl: config.forceAdminSSL ? (config.urlSSL || config.url) : config.url 17 | }; 18 | 19 | data = _.defaults(defaults, options.data); 20 | 21 | // read the proper email body template 22 | return Promise.promisify(fs.readFile)(path.join(templatesDir, options.template + '.html'), 'utf8') 23 | .then(function (content) { 24 | var compiled, 25 | htmlContent, 26 | textContent; 27 | 28 | // insert user-specific data into the email 29 | compiled = _.template(content); 30 | htmlContent = compiled(data); 31 | 32 | // generate a plain-text version of the same email 33 | textContent = htmlToText.fromString(htmlContent); 34 | 35 | return { 36 | html: htmlContent, 37 | text: textContent 38 | }; 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /core/server/middleware/api/version-match.js: -------------------------------------------------------------------------------- 1 | var errors = require('../../errors'), 2 | i18n = require('../../i18n'); 3 | 4 | function checkVersionMatch(req, res, next) { 5 | var requestVersion = req.get('X-Ghost-Version'), 6 | currentVersion = res.locals.safeVersion; 7 | 8 | if (requestVersion && requestVersion !== currentVersion) { 9 | return next(new errors.VersionMismatchError( 10 | i18n.t( 11 | 'errors.middleware.api.versionMismatch', 12 | {requestVersion: requestVersion, currentVersion: currentVersion} 13 | ) 14 | )); 15 | } 16 | 17 | next(); 18 | } 19 | 20 | module.exports = checkVersionMatch; 21 | -------------------------------------------------------------------------------- /core/server/middleware/cache-control.js: -------------------------------------------------------------------------------- 1 | // # CacheControl Middleware 2 | // Usage: cacheControl(profile), where profile is one of 'public' or 'private' 3 | // After: checkIsPrivate 4 | // Before: routes 5 | // App: Admin|Blog|API 6 | // 7 | // Allows each app to declare its own default caching rules 8 | 9 | var _ = require('lodash'), 10 | cacheControl; 11 | 12 | cacheControl = function cacheControl(options) { 13 | /*jslint unparam:true*/ 14 | var profiles = { 15 | public: 'public, max-age=0', 16 | private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' 17 | }, 18 | output; 19 | 20 | if (_.isString(options) && profiles.hasOwnProperty(options)) { 21 | output = profiles[options]; 22 | } 23 | 24 | return function cacheControlHeaders(req, res, next) { 25 | if (output) { 26 | if (res.isPrivateBlog) { 27 | res.set({'Cache-Control': profiles.private}); 28 | } else { 29 | res.set({'Cache-Control': output}); 30 | } 31 | } 32 | next(); 33 | }; 34 | }; 35 | 36 | module.exports = cacheControl; 37 | -------------------------------------------------------------------------------- /core/server/middleware/check-ssl.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'), 2 | url = require('url'), 3 | checkSSL; 4 | 5 | function isSSLrequired(isAdmin, configUrl, forceAdminSSL) { 6 | var forceSSL = url.parse(configUrl).protocol === 'https:' ? true : false; 7 | if (forceSSL || (isAdmin && forceAdminSSL)) { 8 | return true; 9 | } 10 | return false; 11 | } 12 | 13 | // The guts of checkSSL. Indicate forbidden or redirect according to configuration. 14 | // Required args: forceAdminSSL, url and urlSSL should be passed from config. reqURL from req.url 15 | function sslForbiddenOrRedirect(opt) { 16 | var forceAdminSSL = opt.forceAdminSSL, 17 | reqUrl = url.parse(opt.reqUrl), // expected to be relative-to-root 18 | baseUrl = url.parse(opt.configUrlSSL || opt.configUrl), 19 | response = { 20 | // Check if forceAdminSSL: { redirect: false } is set, which means 21 | // we should just deny non-SSL access rather than redirect 22 | isForbidden: (forceAdminSSL && forceAdminSSL.redirect !== undefined && !forceAdminSSL.redirect), 23 | 24 | redirectUrl: function redirectUrl(query) { 25 | return url.format({ 26 | protocol: 'https:', 27 | hostname: baseUrl.hostname, 28 | port: baseUrl.port, 29 | pathname: reqUrl.pathname, 30 | query: query 31 | }); 32 | } 33 | }; 34 | 35 | return response; 36 | } 37 | 38 | // Check to see if we should use SSL 39 | // and redirect if needed 40 | checkSSL = function checkSSL(req, res, next) { 41 | if (isSSLrequired(res.isAdmin, config.url, config.forceAdminSSL)) { 42 | if (!req.secure) { 43 | var response = sslForbiddenOrRedirect({ 44 | forceAdminSSL: config.forceAdminSSL, 45 | configUrlSSL: config.urlSSL, 46 | configUrl: config.url, 47 | reqUrl: req.url 48 | }); 49 | 50 | if (response.isForbidden) { 51 | return res.sendStatus(403); 52 | } else { 53 | return res.redirect(301, response.redirectUrl(req.query)); 54 | } 55 | } 56 | } 57 | next(); 58 | }; 59 | 60 | module.exports = checkSSL; 61 | -------------------------------------------------------------------------------- /core/server/middleware/decide-is-admin.js: -------------------------------------------------------------------------------- 1 | // # DecideIsAdmin Middleware 2 | // Usage: decideIsAdmin(request, result, next) 3 | // After: 4 | // Before: 5 | // App: Blog 6 | // 7 | // Helper function to determine if its an admin page. 8 | 9 | var decideIsAdmin; 10 | 11 | decideIsAdmin = function decideIsAdmin(req, res, next) { 12 | /*jslint unparam:true*/ 13 | res.isAdmin = req.url.lastIndexOf('/ghost/', 0) === 0; 14 | next(); 15 | }; 16 | 17 | module.exports = decideIsAdmin; 18 | -------------------------------------------------------------------------------- /core/server/middleware/labs.js: -------------------------------------------------------------------------------- 1 | var errors = require('../errors'), 2 | labsUtil = require('../utils/labs'), 3 | labs; 4 | 5 | labs = { 6 | subscribers: function subscribers(req, res, next) { 7 | if (labsUtil.isSet('subscribers') === true) { 8 | return next(); 9 | } else { 10 | return errors.handleAPIError(new errors.NotFoundError(), req, res, next); 11 | } 12 | } 13 | }; 14 | 15 | module.exports = labs; 16 | -------------------------------------------------------------------------------- /core/server/middleware/maintenance.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'), 2 | i18n = require('../i18n'), 3 | errors = require('../errors'); 4 | 5 | module.exports = function (req, res, next) { 6 | if (config.maintenance.enabled) { 7 | return next(new errors.Maintenance( 8 | i18n.t('errors.general.maintenance') 9 | )); 10 | } 11 | 12 | next(); 13 | }; 14 | -------------------------------------------------------------------------------- /core/server/middleware/redirect-to-setup.js: -------------------------------------------------------------------------------- 1 | var api = require('../api'), 2 | config = require('../config'); 3 | 4 | // Redirect to setup if no user exists 5 | function redirectToSetup(req, res, next) { 6 | api.authentication.isSetup().then(function then(exists) { 7 | if (!exists.setup[0].status && !req.path.match(/\/setup\//)) { 8 | return res.redirect(config.paths.subdir + '/ghost/setup/'); 9 | } 10 | next(); 11 | }).catch(function handleError(err) { 12 | return next(new Error(err)); 13 | }); 14 | } 15 | 16 | module.exports = redirectToSetup; 17 | -------------------------------------------------------------------------------- /core/server/middleware/serve-shared-file.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | fs = require('fs'), 3 | path = require('path'), 4 | config = require('../config'); 5 | 6 | // ### ServeSharedFile Middleware 7 | // Handles requests to robots.txt and favicon.ico (and caches them) 8 | function serveSharedFile(file, type, maxAge) { 9 | var content, 10 | corePath = config.paths.corePath, 11 | filePath, 12 | blogRegex = /(\{\{blog-url\}\})/g, 13 | apiRegex = /(\{\{api-url\}\})/g; 14 | 15 | filePath = file.match(/^shared/) ? path.join(corePath, file) : path.join(corePath, 'shared', file); 16 | 17 | return function serveSharedFile(req, res, next) { 18 | if (req.path === '/' + file) { 19 | if (content) { 20 | res.writeHead(200, content.headers); 21 | res.end(content.body); 22 | } else { 23 | fs.readFile(filePath, function readFile(err, buf) { 24 | if (err) { 25 | return next(err); 26 | } 27 | 28 | if (type === 'text/xsl' || type === 'text/plain' || type === 'application/javascript') { 29 | buf = buf.toString().replace(blogRegex, config.url.replace(/\/$/, '')); 30 | buf = buf.toString().replace(apiRegex, config.apiUrl()); 31 | } 32 | content = { 33 | headers: { 34 | 'Content-Type': type, 35 | 'Content-Length': buf.length, 36 | ETag: '"' + crypto.createHash('md5').update(buf, 'utf8').digest('hex') + '"', 37 | 'Cache-Control': 'public, max-age=' + maxAge 38 | }, 39 | body: buf 40 | }; 41 | res.writeHead(200, content.headers); 42 | res.end(content.body); 43 | }); 44 | } 45 | } else { 46 | next(); 47 | } 48 | }; 49 | } 50 | 51 | module.exports = serveSharedFile; 52 | -------------------------------------------------------------------------------- /core/server/middleware/static-theme.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | express = require('express'), 3 | path = require('path'), 4 | config = require('../config'), 5 | utils = require('../utils'); 6 | 7 | function isBlackListedFileType(file) { 8 | var blackListedFileTypes = ['.hbs', '.md', '.json'], 9 | ext = path.extname(file); 10 | return _.includes(blackListedFileTypes, ext); 11 | } 12 | 13 | function isWhiteListedFile(file) { 14 | var whiteListedFiles = ['manifest.json'], 15 | base = path.basename(file); 16 | return _.includes(whiteListedFiles, base); 17 | } 18 | 19 | function forwardToExpressStatic(req, res, next) { 20 | if (!req.app.get('activeTheme')) { 21 | next(); 22 | } else { 23 | express.static( 24 | path.join(config.paths.themePath, req.app.get('activeTheme')), 25 | {maxAge: utils.ONE_YEAR_MS} 26 | )(req, res, next); 27 | } 28 | } 29 | 30 | function staticTheme() { 31 | return function blackListStatic(req, res, next) { 32 | if (!isWhiteListedFile(req.path) && isBlackListedFileType(req.path)) { 33 | return next(); 34 | } 35 | return forwardToExpressStatic(req, res, next); 36 | }; 37 | } 38 | 39 | module.exports = staticTheme; 40 | -------------------------------------------------------------------------------- /core/server/middleware/uncapitalise.js: -------------------------------------------------------------------------------- 1 | // # uncapitalise Middleware 2 | // Usage: uncapitalise(req, res, next) 3 | // After: 4 | // Before: 5 | // App: Admin|Blog|API 6 | // 7 | // Detect upper case in req.path. 8 | 9 | var utils = require('../utils'), 10 | uncapitalise; 11 | 12 | uncapitalise = function uncapitalise(req, res, next) { 13 | /*jslint unparam:true*/ 14 | var pathToTest = req.path, 15 | isSignupOrReset = req.path.match(/(\/ghost\/(signup|reset)\/)/i), 16 | isAPI = req.path.match(/(\/ghost\/api\/v[\d\.]+\/.*?\/)/i); 17 | 18 | if (isSignupOrReset) { 19 | pathToTest = isSignupOrReset[1]; 20 | } 21 | 22 | // Do not lowercase anything after /api/v0.1/ to protect :key/:slug 23 | if (isAPI) { 24 | pathToTest = isAPI[1]; 25 | } 26 | 27 | /** 28 | * In node < 0.11.1 req.path is not encoded, afterwards, it is always encoded such that | becomes %7C etc. 29 | * That encoding isn't useful here, as it triggers an extra uncapitalise redirect, so we decode the path first 30 | */ 31 | if (/[A-Z]/.test(decodeURIComponent(pathToTest))) { 32 | res.set('Cache-Control', 'public, max-age=' + utils.ONE_YEAR_S); 33 | // Adding baseUrl ensures subdirectories are kept 34 | res.redirect(301, (req.baseUrl ? req.baseUrl : '') + req.url.replace(pathToTest, pathToTest.toLowerCase())); 35 | } else { 36 | next(); 37 | } 38 | }; 39 | 40 | module.exports = uncapitalise; 41 | -------------------------------------------------------------------------------- /core/server/models/accesstoken.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | Basetoken = require('./base/token'), 3 | events = require('../events'), 4 | 5 | Accesstoken, 6 | Accesstokens; 7 | 8 | Accesstoken = Basetoken.extend({ 9 | tableName: 'accesstokens', 10 | 11 | emitChange: function emitChange(event) { 12 | // Event named 'token' as access and refresh token will be merged in future, see #6626 13 | events.emit('token' + '.' + event, this); 14 | }, 15 | 16 | initialize: function initialize() { 17 | ghostBookshelf.Model.prototype.initialize.apply(this, arguments); 18 | 19 | this.on('created', function onCreated(model) { 20 | model.emitChange('added'); 21 | }); 22 | } 23 | }); 24 | 25 | Accesstokens = ghostBookshelf.Collection.extend({ 26 | model: Accesstoken 27 | }); 28 | 29 | module.exports = { 30 | Accesstoken: ghostBookshelf.model('Accesstoken', Accesstoken), 31 | Accesstokens: ghostBookshelf.collection('Accesstokens', Accesstokens) 32 | }; 33 | -------------------------------------------------------------------------------- /core/server/models/app-field.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | AppField, 3 | AppFields; 4 | 5 | AppField = ghostBookshelf.Model.extend({ 6 | tableName: 'app_fields', 7 | 8 | post: function post() { 9 | return this.morphOne('Post', 'relatable'); 10 | } 11 | }); 12 | 13 | AppFields = ghostBookshelf.Collection.extend({ 14 | model: AppField 15 | }); 16 | 17 | module.exports = { 18 | AppField: ghostBookshelf.model('AppField', AppField), 19 | AppFields: ghostBookshelf.collection('AppFields', AppFields) 20 | }; 21 | -------------------------------------------------------------------------------- /core/server/models/app-setting.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | AppSetting, 3 | AppSettings; 4 | 5 | AppSetting = ghostBookshelf.Model.extend({ 6 | tableName: 'app_settings', 7 | 8 | app: function app() { 9 | return this.belongsTo('App'); 10 | } 11 | }); 12 | 13 | AppSettings = ghostBookshelf.Collection.extend({ 14 | model: AppSetting 15 | }); 16 | 17 | module.exports = { 18 | AppSetting: ghostBookshelf.model('AppSetting', AppSetting), 19 | AppSettings: ghostBookshelf.collection('AppSettings', AppSettings) 20 | }; 21 | -------------------------------------------------------------------------------- /core/server/models/app.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | App, 3 | Apps; 4 | 5 | App = ghostBookshelf.Model.extend({ 6 | tableName: 'apps', 7 | 8 | saving: function saving(newPage, attr, options) { 9 | /*jshint unused:false*/ 10 | var self = this; 11 | 12 | ghostBookshelf.Model.prototype.saving.apply(this, arguments); 13 | 14 | if (this.hasChanged('slug') || !this.get('slug')) { 15 | // Pass the new slug through the generator to strip illegal characters, detect duplicates 16 | return ghostBookshelf.Model.generateSlug(App, this.get('slug') || this.get('name'), 17 | {transacting: options.transacting}) 18 | .then(function then(slug) { 19 | self.set({slug: slug}); 20 | }); 21 | } 22 | }, 23 | 24 | permissions: function permissions() { 25 | return this.belongsToMany('Permission', 'permissions_apps'); 26 | }, 27 | 28 | settings: function settings() { 29 | return this.belongsToMany('AppSetting', 'app_settings'); 30 | } 31 | }, { 32 | /** 33 | * Returns an array of keys permitted in a method's `options` hash, depending on the current method. 34 | * @param {String} methodName The name of the method to check valid options for. 35 | * @return {Array} Keys allowed in the `options` hash of the model's method. 36 | */ 37 | permittedOptions: function permittedOptions(methodName) { 38 | var options = ghostBookshelf.Model.permittedOptions(), 39 | 40 | // whitelists for the `options` hash argument on methods, by method name. 41 | // these are the only options that can be passed to Bookshelf / Knex. 42 | validOptions = { 43 | findOne: ['withRelated'] 44 | }; 45 | 46 | if (validOptions[methodName]) { 47 | options = options.concat(validOptions[methodName]); 48 | } 49 | 50 | return options; 51 | } 52 | }); 53 | 54 | Apps = ghostBookshelf.Collection.extend({ 55 | model: App 56 | }); 57 | 58 | module.exports = { 59 | App: ghostBookshelf.model('App', App), 60 | Apps: ghostBookshelf.collection('Apps', Apps) 61 | }; 62 | -------------------------------------------------------------------------------- /core/server/models/client-trusted-domain.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | 3 | ClientTrustedDomain, 4 | ClientTrustedDomains; 5 | 6 | ClientTrustedDomain = ghostBookshelf.Model.extend({ 7 | tableName: 'client_trusted_domains' 8 | }); 9 | 10 | ClientTrustedDomains = ghostBookshelf.Collection.extend({ 11 | model: ClientTrustedDomain 12 | }); 13 | 14 | module.exports = { 15 | ClientTrustedDomain: ghostBookshelf.model('ClientTrustedDomain', ClientTrustedDomain), 16 | ClientTrustedDomains: ghostBookshelf.collection('ClientTrustedDomains', ClientTrustedDomains) 17 | }; 18 | -------------------------------------------------------------------------------- /core/server/models/client.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | crypto = require('crypto'), 3 | uuid = require('node-uuid'), 4 | 5 | Client, 6 | Clients; 7 | 8 | Client = ghostBookshelf.Model.extend({ 9 | 10 | tableName: 'clients', 11 | 12 | defaults: function defaults() { 13 | var env = process.env.NODE_ENV, 14 | secret = env.indexOf('testing') !== 0 ? crypto.randomBytes(6).toString('hex') : 'not_available'; 15 | 16 | return { 17 | uuid: uuid.v4(), 18 | secret: secret, 19 | status: 'development', 20 | type: 'ua' 21 | }; 22 | }, 23 | 24 | trustedDomains: function trustedDomains() { 25 | return this.hasMany('ClientTrustedDomain', 'client_id'); 26 | } 27 | }, { 28 | /** 29 | * Returns an array of keys permitted in a method's `options` hash, depending on the current method. 30 | * @param {String} methodName The name of the method to check valid options for. 31 | * @return {Array} Keys allowed in the `options` hash of the model's method. 32 | */ 33 | permittedOptions: function permittedOptions(methodName) { 34 | var options = ghostBookshelf.Model.permittedOptions(), 35 | 36 | // whitelists for the `options` hash argument on methods, by method name. 37 | // these are the only options that can be passed to Bookshelf / Knex. 38 | validOptions = { 39 | findOne: ['columns', 'withRelated'] 40 | }; 41 | 42 | if (validOptions[methodName]) { 43 | options = options.concat(validOptions[methodName]); 44 | } 45 | 46 | return options; 47 | } 48 | }); 49 | 50 | Clients = ghostBookshelf.Collection.extend({ 51 | model: Client 52 | }); 53 | 54 | module.exports = { 55 | Client: ghostBookshelf.model('Client', Client), 56 | Clients: ghostBookshelf.collection('Clients', Clients) 57 | }; 58 | -------------------------------------------------------------------------------- /core/server/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var _ = require('lodash'), 6 | exports, 7 | models; 8 | 9 | // enable event listeners 10 | require('./base/listeners'); 11 | 12 | /** 13 | * Expose all models 14 | */ 15 | exports = module.exports; 16 | 17 | models = [ 18 | 'accesstoken', 19 | 'app-field', 20 | 'app-setting', 21 | 'app', 22 | 'client-trusted-domain', 23 | 'client', 24 | 'permission', 25 | 'post', 26 | 'refreshtoken', 27 | 'role', 28 | 'settings', 29 | 'subscriber', 30 | 'tag', 31 | 'user' 32 | ]; 33 | 34 | function init() { 35 | exports.Base = require('./base'); 36 | 37 | models.forEach(function (name) { 38 | _.extend(exports, require('./' + name)); 39 | }); 40 | } 41 | 42 | /** 43 | * Expose `init` 44 | */ 45 | 46 | exports.init = init; 47 | -------------------------------------------------------------------------------- /core/server/models/permission.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | 3 | Permission, 4 | Permissions; 5 | 6 | Permission = ghostBookshelf.Model.extend({ 7 | 8 | tableName: 'permissions', 9 | 10 | roles: function roles() { 11 | return this.belongsToMany('Role'); 12 | }, 13 | 14 | users: function users() { 15 | return this.belongsToMany('User'); 16 | }, 17 | 18 | apps: function apps() { 19 | return this.belongsToMany('App'); 20 | } 21 | }); 22 | 23 | Permissions = ghostBookshelf.Collection.extend({ 24 | model: Permission 25 | }); 26 | 27 | module.exports = { 28 | Permission: ghostBookshelf.model('Permission', Permission), 29 | Permissions: ghostBookshelf.collection('Permissions', Permissions) 30 | }; 31 | -------------------------------------------------------------------------------- /core/server/models/plugins/access-rules.js: -------------------------------------------------------------------------------- 1 | // # Access Rules 2 | // 3 | // Extends Bookshelf.Model.force to take a 'context' option which provides information on how this query should 4 | // be treated in terms of data access rules - currently just detecting public requests 5 | module.exports = function (Bookshelf) { 6 | var model = Bookshelf.Model, 7 | Model; 8 | 9 | Model = Bookshelf.Model.extend({ 10 | /** 11 | * Cached copy of the context setup for this model instance 12 | */ 13 | _context: null, 14 | 15 | /** 16 | * ## Is Public Context? 17 | * A helper to determine if this is a public request or not 18 | * @returns {boolean} 19 | */ 20 | isPublicContext: function isPublicContext() { 21 | return !!(this._context && this._context.public); 22 | }, 23 | 24 | isInternalContext: function isInternalContext() { 25 | return !!(this._context && this._context.internal); 26 | } 27 | }, 28 | { 29 | /** 30 | * ## Forge 31 | * Ensure that context gets set as part of the forge 32 | * 33 | * @param {object} attributes 34 | * @param {object} options 35 | * @returns {Bookshelf.Model} model 36 | */ 37 | forge: function forge(attributes, options) { 38 | var self = model.forge.apply(this, arguments); 39 | 40 | if (options && options.context) { 41 | self._context = options.context; 42 | delete options.context; 43 | } 44 | 45 | return self; 46 | } 47 | }); 48 | 49 | Bookshelf.Model = Model; 50 | }; 51 | -------------------------------------------------------------------------------- /core/server/models/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessRules: require('./access-rules'), 3 | filter: require('./filter'), 4 | includeCount: require('./include-count'), 5 | pagination: require('./pagination') 6 | }; 7 | -------------------------------------------------------------------------------- /core/server/models/refreshtoken.js: -------------------------------------------------------------------------------- 1 | var ghostBookshelf = require('./base'), 2 | Basetoken = require('./base/token'), 3 | 4 | Refreshtoken, 5 | Refreshtokens; 6 | 7 | Refreshtoken = Basetoken.extend({ 8 | tableName: 'refreshtokens' 9 | }); 10 | 11 | Refreshtokens = ghostBookshelf.Collection.extend({ 12 | model: Refreshtoken 13 | }); 14 | 15 | module.exports = { 16 | Refreshtoken: ghostBookshelf.model('Refreshtoken', Refreshtoken), 17 | Refreshtokens: ghostBookshelf.collection('Refreshtokens', Refreshtokens) 18 | }; 19 | -------------------------------------------------------------------------------- /core/server/overrides.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment-timezone'); 2 | 3 | /** 4 | * force UTC 5 | * - you can require moment or moment-timezone, both is configured to UTC 6 | * - you are allowed to use new Date() to instantiate datetime values for models, because they are transformed into UTC in the model layer 7 | * - be careful when not working with models, every value from the native JS Date is local TZ 8 | * - be careful when you work with date operations, therefor always wrap a date into moment 9 | */ 10 | moment.tz.setDefault('UTC'); 11 | -------------------------------------------------------------------------------- /core/server/permissions/effective.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | Models = require('../models'), 4 | errors = require('../errors'), 5 | i18n = require('../i18n'), 6 | effective; 7 | 8 | effective = { 9 | user: function (id) { 10 | return Models.User.findOne({id: id, status: 'all'}, {include: ['permissions', 'roles', 'roles.permissions']}) 11 | .then(function (foundUser) { 12 | // CASE: {context: {user: id}} where the id is not in our database 13 | if (!foundUser) { 14 | return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.userNotFound'))); 15 | } 16 | 17 | var seenPerms = {}, 18 | rolePerms = _.map(foundUser.related('roles').models, function (role) { 19 | return role.related('permissions').models; 20 | }), 21 | allPerms = [], 22 | user = foundUser.toJSON(); 23 | 24 | rolePerms.push(foundUser.related('permissions').models); 25 | 26 | _.each(rolePerms, function (rolePermGroup) { 27 | _.each(rolePermGroup, function (perm) { 28 | var key = perm.get('action_type') + '-' + perm.get('object_type') + '-' + perm.get('object_id'); 29 | 30 | // Only add perms once 31 | if (seenPerms[key]) { 32 | return; 33 | } 34 | 35 | allPerms.push(perm); 36 | seenPerms[key] = true; 37 | }); 38 | }); 39 | 40 | return {permissions: allPerms, roles: user.roles}; 41 | }, errors.logAndThrowError); 42 | }, 43 | 44 | app: function (appName) { 45 | return Models.App.findOne({name: appName}, {withRelated: ['permissions']}) 46 | .then(function (foundApp) { 47 | if (!foundApp) { 48 | return []; 49 | } 50 | 51 | return {permissions: foundApp.related('permissions').models}; 52 | }, errors.logAndThrowError); 53 | } 54 | }; 55 | 56 | module.exports = effective; 57 | -------------------------------------------------------------------------------- /core/server/routes/admin.js: -------------------------------------------------------------------------------- 1 | var admin = require('../controllers/admin'), 2 | express = require('express'), 3 | 4 | adminRoutes; 5 | 6 | adminRoutes = function () { 7 | var router = express.Router(); 8 | 9 | router.get('*', admin.index); 10 | 11 | return router; 12 | }; 13 | 14 | module.exports = adminRoutes; 15 | -------------------------------------------------------------------------------- /core/server/routes/frontend.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | path = require('path'), 3 | config = require('../config'), 4 | frontend = require('../controllers/frontend'), 5 | channels = require('../controllers/frontend/channels'), 6 | utils = require('../utils'), 7 | 8 | frontendRoutes; 9 | 10 | frontendRoutes = function frontendRoutes() { 11 | var router = express.Router(), 12 | subdir = config.paths.subdir, 13 | routeKeywords = config.routeKeywords; 14 | 15 | // ### Admin routes 16 | router.get(/^\/(logout|signout)\/$/, function redirectToSignout(req, res) { 17 | utils.redirect301(res, subdir + '/ghost/signout/'); 18 | }); 19 | router.get(/^\/signup\/$/, function redirectToSignup(req, res) { 20 | utils.redirect301(res, subdir + '/ghost/signup/'); 21 | }); 22 | 23 | // redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc. 24 | router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin|login)\/?)$/, function redirectToAdmin(req, res) { 25 | utils.redirect301(res, subdir + '/ghost/'); 26 | }); 27 | 28 | // Post Live Preview 29 | router.get('/' + routeKeywords.preview + '/:uuid', frontend.preview); 30 | 31 | // Channels 32 | router.use(channels.router()); 33 | 34 | // Default 35 | router.get('*', frontend.single); 36 | 37 | // setup routes for internal apps 38 | // @TODO: refactor this to be a proper app route hook for internal & external apps 39 | config.internalApps.forEach(function (appName) { 40 | var app = require(path.join(config.paths.internalAppPath, appName)); 41 | if (app.hasOwnProperty('setupRoutes')) { 42 | app.setupRoutes(router); 43 | } 44 | }); 45 | 46 | return router; 47 | }; 48 | 49 | module.exports = frontendRoutes; 50 | -------------------------------------------------------------------------------- /core/server/routes/index.js: -------------------------------------------------------------------------------- 1 | var api = require('./api'), 2 | admin = require('./admin'), 3 | frontend = require('./frontend'); 4 | 5 | module.exports = { 6 | apiBaseUri: '/ghost/api/v0.1/', 7 | api: api, 8 | admin: admin, 9 | frontend: frontend 10 | }; 11 | -------------------------------------------------------------------------------- /core/server/scheduling/SchedulingBase.js: -------------------------------------------------------------------------------- 1 | function SchedulingBase() { 2 | Object.defineProperty(this, 'requiredFns', { 3 | value: ['schedule', 'unschedule', 'reschedule', 'run'], 4 | writable: false 5 | }); 6 | } 7 | 8 | module.exports = SchedulingBase; 9 | -------------------------------------------------------------------------------- /core/server/scheduling/index.js: -------------------------------------------------------------------------------- 1 | var postScheduling = require(__dirname + '/post-scheduling'); 2 | 3 | /** 4 | * scheduling modules: 5 | * - post scheduling: publish posts/pages when scheduled 6 | */ 7 | exports.init = function init(options) { 8 | options = options || {}; 9 | 10 | return postScheduling.init(options); 11 | }; 12 | -------------------------------------------------------------------------------- /core/server/scheduling/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Promise = require('bluebird'), 3 | SchedulingBase = require(__dirname + '/SchedulingBase'), 4 | errors = require(__dirname + '/../errors'); 5 | 6 | exports.createAdapter = function (options) { 7 | options = options || {}; 8 | 9 | var adapter = null, 10 | activeAdapter = options.active, 11 | path = options.path; 12 | 13 | if (!activeAdapter) { 14 | return Promise.reject(new errors.IncorrectUsage('Please provide an active adapter.')); 15 | } 16 | 17 | /** 18 | * CASE: active adapter is a npm module 19 | */ 20 | try { 21 | adapter = new (require(activeAdapter))(options); 22 | } catch (err) { 23 | if (err.code !== 'MODULE_NOT_FOUND') { 24 | return Promise.reject(new errors.IncorrectUsage(err.message)); 25 | } 26 | } 27 | 28 | /** 29 | * CASE: active adapter is located in specific ghost path 30 | */ 31 | try { 32 | adapter = adapter || new (require(path + activeAdapter))(options); 33 | } catch (err) { 34 | if (err.code === 'MODULE_NOT_FOUND') { 35 | return Promise.reject(new errors.IncorrectUsage('MODULE_NOT_FOUND', activeAdapter)); 36 | } 37 | 38 | return Promise.reject(new errors.IncorrectUsage(err.message)); 39 | } 40 | 41 | if (!(adapter instanceof SchedulingBase)) { 42 | return Promise.reject(new errors.IncorrectUsage('Your adapter does not inherit from the SchedulingBase.')); 43 | } 44 | 45 | if (!adapter.requiredFns) { 46 | return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.')); 47 | } 48 | 49 | if (_.xor(adapter.requiredFns, Object.keys(_.pick(Object.getPrototypeOf(adapter), adapter.requiredFns))).length) { 50 | return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.')); 51 | } 52 | 53 | return Promise.resolve(adapter); 54 | }; 55 | -------------------------------------------------------------------------------- /core/server/storage/base.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'), 2 | path = require('path'); 3 | 4 | function StorageBase() { 5 | } 6 | 7 | StorageBase.prototype.getTargetDir = function (baseDir) { 8 | var m = moment(), 9 | month = m.format('MM'), 10 | year = m.format('YYYY'); 11 | 12 | if (baseDir) { 13 | return path.join(baseDir, year, month); 14 | } 15 | 16 | return path.join(year, month); 17 | }; 18 | 19 | StorageBase.prototype.generateUnique = function (store, dir, name, ext, i) { 20 | var self = this, 21 | filename, 22 | append = ''; 23 | 24 | if (i) { 25 | append = '-' + i; 26 | } 27 | 28 | filename = path.join(dir, name + append + ext); 29 | 30 | return store.exists(filename).then(function (exists) { 31 | if (exists) { 32 | i = i + 1; 33 | return self.generateUnique(store, dir, name, ext, i); 34 | } else { 35 | return filename; 36 | } 37 | }); 38 | }; 39 | 40 | StorageBase.prototype.getUniqueFileName = function (store, image, targetDir) { 41 | var ext = path.extname(image.name), 42 | name = path.basename(image.name, ext).replace(/[^\w@]/gi, '-'), 43 | self = this; 44 | 45 | return self.generateUnique(store, targetDir, name, ext, 0); 46 | }; 47 | 48 | module.exports = StorageBase; 49 | -------------------------------------------------------------------------------- /core/server/storage/index.js: -------------------------------------------------------------------------------- 1 | var errors = require('../errors'), 2 | config = require('../config'), 3 | storage = {}; 4 | 5 | function getStorage(storageChoice) { 6 | var storagePath, 7 | storageConfig; 8 | 9 | storageChoice = config.storage.active; 10 | storagePath = config.paths.storage; 11 | storageConfig = config.storage[storageChoice]; 12 | 13 | if (storage[storageChoice]) { 14 | return storage[storageChoice]; 15 | } 16 | 17 | try { 18 | // TODO: determine if storage has all the necessary methods. 19 | storage[storageChoice] = require(storagePath); 20 | } catch (e) { 21 | errors.logError(e); 22 | } 23 | 24 | // Instantiate and cache the storage module instance. 25 | storage[storageChoice] = new storage[storageChoice](storageConfig); 26 | 27 | return storage[storageChoice]; 28 | } 29 | 30 | module.exports.getStorage = getStorage; 31 | -------------------------------------------------------------------------------- /core/server/utils/gravatar.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | config = require('../config'), 3 | crypto = require('crypto'), 4 | https = require('https'); 5 | 6 | module.exports.lookup = function lookup(userData, timeout) { 7 | var gravatarUrl = '//www.gravatar.com/avatar/' + 8 | crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') + 9 | '?s=250'; 10 | 11 | return new Promise(function gravatarRequest(resolve) { 12 | if (config.isPrivacyDisabled('useGravatar') || process.env.NODE_ENV.indexOf('testing') > -1) { 13 | return resolve(userData); 14 | } 15 | 16 | var request, timer, timerEnded = false; 17 | 18 | request = https.get('https:' + gravatarUrl + '&d=404&r=x', function (response) { 19 | clearTimeout(timer); 20 | if (response.statusCode !== 404 && !timerEnded) { 21 | gravatarUrl += '&d=mm&r=x'; 22 | userData.image = gravatarUrl; 23 | } 24 | 25 | resolve(userData); 26 | }); 27 | 28 | request.on('error', function () { 29 | clearTimeout(timer); 30 | // just resolve with no image url 31 | if (!timerEnded) { 32 | return resolve(userData); 33 | } 34 | }); 35 | 36 | timer = setTimeout(function () { 37 | timerEnded = true; 38 | request.abort(); 39 | return resolve(userData); 40 | }, timeout || 2000); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /core/server/utils/labs.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'), 2 | flagIsSet; 3 | 4 | flagIsSet = function flagIsSet(flag) { 5 | var labsConfig = config.labs; 6 | 7 | return labsConfig && labsConfig[flag] && labsConfig[flag] === true; 8 | }; 9 | 10 | module.exports.isSet = flagIsSet; 11 | -------------------------------------------------------------------------------- /core/server/utils/npm/preinstall.js: -------------------------------------------------------------------------------- 1 | var validVersions = process.env.npm_package_engines_node.split(' || '), 2 | currentVersion = process.versions.node, 3 | foundMatch = false, 4 | majMinRegex = /(\d+\.\d+)/, 5 | majorRegex = /^\d+/, 6 | minorRegex = /\d+$/, 7 | exitCodes = { 8 | NODE_VERSION_UNSUPPORTED: 231 9 | }; 10 | 11 | function doError() { 12 | console.error('\x1B[31mERROR: Unsupported version of Node'); 13 | console.error('\x1B[37mGhost supports LTS Node versions: ' + process.env.npm_package_engines_node); 14 | console.error('You are currently using version: ' + process.versions.node + '\033[0m'); 15 | console.error('\x1B[32mThis check can be overridden, see http://support.ghost.org/supported-node-versions/ for more info\033[0m'); 16 | 17 | process.exit(exitCodes.NODE_VERSION_UNSUPPORTED); 18 | } 19 | 20 | if (process.env.GHOST_NODE_VERSION_CHECK === 'false') { 21 | console.log('\x1B[33mSkipping Node version check\033[0m'); 22 | } else { 23 | try { 24 | currentVersion = currentVersion.match(majMinRegex)[0]; 25 | 26 | validVersions.forEach(function (version) { 27 | var matchChar = version.charAt(0), 28 | versionString = version.match(majMinRegex)[0]; 29 | 30 | if ( 31 | (matchChar === '~' && currentVersion === versionString) 32 | || (matchChar === '^' 33 | && currentVersion.match(majorRegex)[0] === versionString.match(majorRegex)[0] 34 | && currentVersion.match(minorRegex)[0] >= versionString.match(minorRegex)[0] 35 | ) 36 | ) { 37 | foundMatch = true; 38 | } 39 | }); 40 | 41 | if (foundMatch !== true) { 42 | doError(); 43 | } 44 | } catch (e) { 45 | doError(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/server/utils/parse-package-json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Promise = require('bluebird'), 6 | fs = require('fs'), 7 | i18n = require('../i18n'), 8 | 9 | readFile = Promise.promisify(fs.readFile); 10 | 11 | /** 12 | * Parse package.json and validate it has 13 | * all the required fields 14 | */ 15 | 16 | function parsePackageJson(path) { 17 | return readFile(path) 18 | .catch(function () { 19 | var err = new Error(i18n.t('errors.utils.parsepackagejson.couldNotReadPackage')); 20 | err.context = path; 21 | 22 | return Promise.reject(err); 23 | }) 24 | .then(function (source) { 25 | var hasRequiredKeys, json, err; 26 | 27 | try { 28 | json = JSON.parse(source); 29 | 30 | hasRequiredKeys = json.name && json.version; 31 | 32 | if (!hasRequiredKeys) { 33 | err = new Error(i18n.t('errors.utils.parsepackagejson.nameOrVersionMissing')); 34 | err.context = path; 35 | err.help = i18n.t('errors.utils.parsepackagejson.willBeRequired', {url: 'http://docs.ghost.org/themes/'}); 36 | 37 | return Promise.reject(err); 38 | } 39 | 40 | return json; 41 | } catch (parseError) { 42 | err = new Error(i18n.t('errors.utils.parsepackagejson.themeFileIsMalformed')); 43 | err.context = path; 44 | err.help = i18n.t('errors.utils.parsepackagejson.willBeRequired', {url: 'http://docs.ghost.org/themes/'}); 45 | 46 | return Promise.reject(err); 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * Expose `parsePackageJson` 53 | */ 54 | 55 | module.exports = parsePackageJson; 56 | -------------------------------------------------------------------------------- /core/server/utils/pipeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Pipeline Utility 3 | * 4 | * Based on pipeline.js from when.js: 5 | * https://github.com/cujojs/when/blob/3.7.4/pipeline.js 6 | */ 7 | var Promise = require('bluebird'); 8 | 9 | function pipeline(tasks /* initial arguments */) { 10 | var args = Array.prototype.slice.call(arguments, 1), 11 | 12 | runTask = function (task, args) { 13 | // Self-optimizing function to run first task with multiple 14 | // args using apply, but subsequent tasks via direct invocation 15 | runTask = function (task, arg) { 16 | return task(arg); 17 | }; 18 | 19 | return task.apply(null, args); 20 | }; 21 | 22 | // Resolve any promises for the arguments passed in first 23 | return Promise.all(args).then(function (args) { 24 | // Iterate through the tasks passing args from one into the next 25 | return Promise.reduce(tasks, function (arg, task) { 26 | return runTask(task, arg); 27 | }, args); 28 | }); 29 | } 30 | 31 | module.exports = pipeline; 32 | -------------------------------------------------------------------------------- /core/server/utils/read-csv.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | csvParser = require('csv-parser'), 3 | _ = require('lodash'), 4 | fs = require('fs'); 5 | 6 | function readCSV(options) { 7 | var columnsToExtract = options.columnsToExtract || [], 8 | results = [], rows = []; 9 | 10 | return new Promise(function (resolve, reject) { 11 | var readFile = fs.createReadStream(options.path); 12 | 13 | readFile.on('err', function (err) { 14 | reject(err); 15 | }) 16 | .pipe(csvParser()) 17 | .on('data', function (row) { 18 | rows.push(row); 19 | }) 20 | .on('end', function () { 21 | // If CSV is single column - return all values including header 22 | var headers = _.keys(rows[0]), result = {}, columnMap = {}; 23 | if (columnsToExtract.length === 1 && headers.length === 1) { 24 | results = _.map(rows, function (value) { 25 | result = {}; 26 | result[columnsToExtract[0].name] = value[headers[0]]; 27 | return result; 28 | }); 29 | 30 | // Add first row 31 | result = {}; 32 | result[columnsToExtract[0].name] = headers[0]; 33 | results = [result].concat(results); 34 | } else { 35 | // If there are multiple columns in csv file 36 | // try to match headers using lookup value 37 | 38 | _.map(columnsToExtract, function findMatches(column) { 39 | _.each(headers, function checkheader(header) { 40 | if (column.lookup.test(header)) { 41 | columnMap[column.name] = header; 42 | } 43 | }); 44 | }); 45 | 46 | results = _.map(rows, function evaluateRow(row) { 47 | var result = {}; 48 | _.each(columnMap, function returnMatches(value, key) { 49 | result[key] = row[value]; 50 | }); 51 | return result; 52 | }); 53 | } 54 | resolve(results); 55 | }); 56 | }); 57 | } 58 | 59 | module.exports = readCSV; 60 | -------------------------------------------------------------------------------- /core/server/utils/read-themes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var readDirectory = require('./read-directory'), 6 | Promise = require('bluebird'), 7 | join = require('path').join, 8 | fs = require('fs'), 9 | 10 | statFile = Promise.promisify(fs.stat); 11 | 12 | /** 13 | * Read themes 14 | */ 15 | 16 | function readThemes(dir) { 17 | var originalTree; 18 | 19 | return readDirectory(dir) 20 | .tap(function (tree) { 21 | originalTree = tree; 22 | }) 23 | .then(Object.keys) 24 | .filter(function (file) { 25 | var path = join(dir, file); 26 | 27 | return statFile(path).then(function (stat) { 28 | return stat.isDirectory(); 29 | }); 30 | }) 31 | .then(function (directories) { 32 | var themes = {}; 33 | 34 | directories.forEach(function (name) { 35 | themes[name] = originalTree[name]; 36 | }); 37 | 38 | return themes; 39 | }); 40 | } 41 | 42 | /** 43 | * Expose `read-themes` 44 | */ 45 | 46 | module.exports = readThemes; 47 | -------------------------------------------------------------------------------- /core/server/utils/sequence.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | /** 4 | * expects an array of functions returning a promise 5 | */ 6 | function sequence(tasks /* Any Arguments */) { 7 | var args = Array.prototype.slice.call(arguments, 1); 8 | 9 | return Promise.reduce(tasks, function (results, task) { 10 | return task.apply(this, args).then(function (result) { 11 | results.push(result); 12 | return results; 13 | }); 14 | }, []); 15 | } 16 | 17 | module.exports = sequence; 18 | -------------------------------------------------------------------------------- /core/server/utils/social-urls.js: -------------------------------------------------------------------------------- 1 | module.exports.twitterUrl = function twitterUrl(username) { 2 | // Creates the canonical twitter URL without the '@' 3 | return 'https://twitter.com/' + username.replace(/^@/, ''); 4 | }; 5 | 6 | module.exports.facebookUrl = function facebookUrl(username) { 7 | // Handles a starting slash, this shouldn't happen, but just in case 8 | return 'https://www.facebook.com/' + username.replace(/^\//, ''); 9 | }; 10 | -------------------------------------------------------------------------------- /core/server/utils/validate-themes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var readThemes = require('./read-themes'), 6 | Promise = require('bluebird'), 7 | _ = require('lodash'), 8 | i18n = require('../i18n'); 9 | 10 | /** 11 | * Validate themes: 12 | * 13 | * 1. Check if theme has package.json 14 | */ 15 | 16 | function validateThemes(dir) { 17 | var result = { 18 | warnings: [], 19 | errors: [] 20 | }; 21 | 22 | return readThemes(dir) 23 | .tap(function (themes) { 24 | _.each(themes, function (theme, name) { 25 | var hasPackageJson, warning; 26 | 27 | hasPackageJson = theme['package.json'] !== undefined; 28 | 29 | if (!hasPackageJson) { 30 | warning = { 31 | message: i18n.t('errors.utils.validatethemes.themeWithNoPackage.message'), 32 | context: i18n.t('errors.utils.validatethemes.themeWithNoPackage.context', {name: name}), 33 | help: i18n.t('errors.utils.validatethemes.themeWithNoPackage.help', {url: 'http://docs.ghost.org/themes/'}) 34 | }; 35 | 36 | result.warnings.push(warning); 37 | } 38 | 39 | // if package.json is `null`, it means that it exists 40 | // but JSON.parse failed (invalid json syntax) 41 | if (hasPackageJson && theme['package.json'] === null) { 42 | warning = { 43 | message: i18n.t('errors.utils.validatethemes.malformedPackage.message'), 44 | context: i18n.t('errors.utils.validatethemes.malformedPackage.context', {name: name}), 45 | help: i18n.t('errors.utils.validatethemes.malformedPackage.help', {url: 'http://docs.ghost.org/themes/'}) 46 | }; 47 | 48 | result.warnings.push(warning); 49 | } 50 | }); 51 | }) 52 | .then(function () { 53 | var hasNotifications = result.warnings.length || result.errors.length; 54 | 55 | if (hasNotifications) { 56 | return Promise.reject(result); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * Expose `validateThemes` 63 | */ 64 | 65 | module.exports = validateThemes; 66 | -------------------------------------------------------------------------------- /core/shared/favicon.ico: -------------------------------------------------------------------------------- 1 |  ((  0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @ -------------------------------------------------------------------------------- /core/shared/ghost-url.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var apiUrl = '{{api-url}}', 5 | clientId, 6 | clientSecret, 7 | url, 8 | init; 9 | 10 | function generateQueryString(object) { 11 | var queries = [], 12 | i; 13 | 14 | if (!object) { 15 | return ''; 16 | } 17 | 18 | for (i in object) { 19 | if (object.hasOwnProperty(i) && (!!object[i] || object[i] === false)) { 20 | queries.push(i + '=' + encodeURIComponent(object[i])); 21 | } 22 | } 23 | 24 | if (queries.length) { 25 | return '?' + queries.join('&'); 26 | } 27 | return ''; 28 | } 29 | 30 | url = { 31 | api: function () { 32 | var args = Array.prototype.slice.call(arguments), 33 | queryOptions, 34 | requestUrl = apiUrl; 35 | 36 | queryOptions = args.pop(); 37 | 38 | if (queryOptions && typeof queryOptions !== 'object') { 39 | args.push(queryOptions); 40 | queryOptions = {}; 41 | } 42 | 43 | queryOptions = queryOptions || {}; 44 | 45 | queryOptions.client_id = clientId; 46 | queryOptions.client_secret = clientSecret; 47 | 48 | if (args.length) { 49 | args.forEach(function (el) { 50 | requestUrl += el.replace(/^\/|\/$/g, '') + '/'; 51 | }); 52 | } 53 | 54 | return requestUrl + generateQueryString(queryOptions); 55 | } 56 | }; 57 | 58 | init = function (options) { 59 | clientId = options.clientId ? options.clientId : ''; 60 | clientSecret = options.clientSecret ? options.clientSecret : ''; 61 | apiUrl = options.url ? options.url : (apiUrl.match(/{\{api-url}}/) ? '' : apiUrl); 62 | }; 63 | 64 | if (typeof window !== 'undefined') { 65 | window.ghost = window.ghost || {}; 66 | window.ghost.url = url; 67 | window.ghost.init = init; 68 | } 69 | 70 | if (typeof module !== 'undefined') { 71 | module.exports = { 72 | url: url, 73 | init: init 74 | }; 75 | } 76 | })(); 77 | -------------------------------------------------------------------------------- /core/shared/ghost-url.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function a(a){var b,c=[];if(!a)return"";for(b in a)a.hasOwnProperty(b)&&(a[b]||a[b]===!1)&&c.push(b+"="+encodeURIComponent(a[b]));return c.length?"?"+c.join("&"):""}var b,c,d,e,f="{{api-url}}";d={api:function(){var d,e=Array.prototype.slice.call(arguments),g=f;return d=e.pop(),d&&"object"!=typeof d&&(e.push(d),d={}),d=d||{},d.client_id=b,d.client_secret=c,e.length&&e.forEach(function(a){g+=a.replace(/^\/|\/$/g,"")+"/"}),g+a(d)}},e=function(a){b=a.clientId?a.clientId:"",c=a.clientSecret?a.clientSecret:"",f=a.url?a.url:f.match(/{\{api-url}}/)?"":f},"undefined"!=typeof window&&(window.ghost=window.ghost||{},window.ghost.url=d,window.ghost.init=e),"undefined"!=typeof module&&(module.exports={url:d,init:e})}(); -------------------------------------------------------------------------------- /core/shared/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Sitemap: {{blog-url}}/sitemap.xml 3 | Disallow: /ghost/ 4 | -------------------------------------------------------------------------------- /docs/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureWebApps/Ghost-Azure/1d2954261f87216a769ed91d2fe82cdedd2bdadb/docs/update.png -------------------------------------------------------------------------------- /iisnode.yml: -------------------------------------------------------------------------------- 1 | node_env: production 2 | loggingEnabled: true 3 | enableXFF: true 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // # Ghost Startup 2 | // Orchestrates the startup of Ghost when run from command line. 3 | 4 | var express, 5 | ghost, 6 | parentApp, 7 | errors; 8 | 9 | require('./core/server/overrides'); 10 | 11 | // Make sure dependencies are installed and file system permissions are correct. 12 | require('./core/server/utils/startup-check').check(); 13 | 14 | // Proceed with startup 15 | express = require('express'); 16 | ghost = require('./core'); 17 | errors = require('./core/server/errors'); 18 | 19 | // Create our parent express app instance. 20 | parentApp = express(); 21 | 22 | // Call Ghost to get an instance of GhostServer 23 | ghost().then(function (ghostServer) { 24 | // Mount our Ghost instance on our desired subdirectory path if it exists. 25 | parentApp.use(ghostServer.config.paths.subdir, ghostServer.rootApp); 26 | 27 | // Let Ghost handle starting our server instance. 28 | ghostServer.start(parentApp); 29 | }).catch(function (err) { 30 | errors.logErrorAndExit(err, err.context, err.help); 31 | }); 32 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------