Commit e0c9293c authored by kaiyou's avatar kaiyou

Merge branch 'master' of https://github.com/writeas/writefreely

parents db0f31b7 7a07e100
Pipeline #180 failed with stage
in 2 minutes and 27 seconds
---
name: Bug report
about: Let us know what went wrong.
labels: ❓ bug
---
### Describe the bug
......
......@@ -3,6 +3,7 @@
*.swo
build
tmp
*.ini
*.db
......
language: go
go:
- "1.10.x"
- "1.11.x"
env:
- GO111MODULE=on
script: make ci
......@@ -9,9 +9,13 @@ GOGET=$(GOCMD) get
BINARY_NAME=writefreely
DOCKERCMD=docker
IMAGE_NAME=writeas/writefreely
TMPBIN=./tmp
all : build
ci: ci-assets deps
cd cmd/writefreely; $(GOBUILD) -v
build: assets deps
cd cmd/writefreely; $(GOBUILD) -v -tags='sqlite'
......@@ -47,13 +51,15 @@ run: dev-assets
$(BINARY_NAME) --debug
deps :
$(GOGET) -tags='sqlite' -v ./...
$(GOGET) -tags='sqlite' -d -v ./...
deps-no-sqlite:
$(GOGET) -v ./...
$(GOGET) -d -v ./...
install : build
cmd/writefreely/$(BINARY_NAME) --config
cmd/writefreely/$(BINARY_NAME) --gen-keys
cmd/writefreely/$(BINARY_NAME) --init-db
cd less/; $(MAKE) install $(MFLAGS)
release : clean ui assets
......@@ -107,8 +113,21 @@ generate :
$(GOGET) -u github.com/jteeuwen/go-bindata/...; \
fi
$(TMPBIN):
mkdir -p $(TMPBIN)
$(TMPBIN)/go-bindata: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/go-bindata github.com/jteeuwen/go-bindata/go-bindata
$(TMPBIN)/xgo: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo
ci-assets : $(TMPBIN)/go-bindata
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql
clean :
-rm -rf build
-rm -rf tmp
cd less/; $(MAKE) clean $(MFLAGS)
force_look :
......
 
<p align="center">
<a href="https://writefreely.org"><img src="https://writefreely.org/img/writefreely.svg" width="350px" alt="Write Freely" /></a>
<a href="https://writefreely.org"><img src="https://writefreely.org/img/writefreely.svg" width="350px" alt="WriteFreely" /></a>
</p>
<hr />
<p align="center">
......@@ -97,59 +97,11 @@ WriteFreely is available in these package repositories:
## Development
Ready to hack on your site? Here's a quick overview.
### Prerequisites
* [Go 1.10+](https://golang.org/dl/)
* [Node.js](https://nodejs.org/en/download/)
### Setting up
```bash
go get -d github.com/writeas/writefreely/cmd/writefreely
```
Configure your site, create your database, and import the schema [as shown above](#quick-start). Then generate the remaining files you'll need:
```bash
make install # Generates encryption keys; installs LESS compiler
make ui # Generates CSS (run this whenever you update your styles)
make run # Runs the application
```
Ready to hack on your site? Get started with our [developer guide](https://writefreely.org/docs/latest/developer).
## Docker
### Using Docker for Development
If you'd like to use Docker as a base for working on a site's styles and such,
you can run the following from a Bash shell.
*Note: This process is intended only for working on site styling. If you'd
like to run Write Freely in production as a Docker service, it'll require a
little more work.*
The `docker-setup.sh` script will present you with a few questions to set up
your dev instance. You can hit enter for most of them, except for "Admin username"
and "Admin password." You'll probably have to wait a few seconds after running
`docker-compose up -d` for the Docker services to come up before running the
bash script.
```
docker-compose up -d
./docker-setup.sh
```
Now you should be able to navigate to http://localhost:8080 and start working!
When you're completely done working, you can run `docker-compose down` to destroy
your virtual environment, including your database data. Otherwise, `docker-compose stop`
will shut down your environment without destroying your data.
### Using Docker for Production
Write Freely doesn't yet provide an official Docker pathway to production. We're
working on it, though!
Read about using Docker in the [documentation](https://writefreely.org/docs/latest/admin/docker).
## Contributing
......
......@@ -11,6 +11,7 @@
package writefreely
import (
"database/sql"
"fmt"
"github.com/gogits/gogs/pkg/tool"
"github.com/gorilla/mux"
......@@ -78,6 +79,23 @@ type inspectedCollection struct {
LastPost string
}
type instanceContent struct {
ID string
Type string
Title sql.NullString
Content string
Updated time.Time
}
func (c instanceContent) UpdatedFriendly() string {
/*
// TODO: accept a locale in this method and use that for the format
var loc monday.Locale = monday.LocaleEnUS
return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
*/
return c.Updated.Format("January 2, 2006, 3:04 PM")
}
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
updateAppStats()
p := struct {
......@@ -86,8 +104,6 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
Config config.AppCfg
Message, ConfigMessage string
AboutPage, PrivacyPage string
}{
UserPage: NewUserPage(app, r, u, "Admin", nil),
SysStatus: sysStatus,
......@@ -97,17 +113,6 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
ConfigMessage: r.FormValue("cm"),
}
var err error
p.AboutPage, err = getAboutPage(app)
if err != nil {
return err
}
p.PrivacyPage, _, err = getPrivacyPage(app)
if err != nil {
return err
}
showUserPage(w, "admin", p)
return nil
}
......@@ -224,6 +229,106 @@ func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Reque
return nil
}
func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct {
*UserPage
Config config.AppCfg
Message string
Pages []*instanceContent
}{
UserPage: NewUserPage(app, r, u, "Pages", nil),
Config: app.cfg.App,
Message: r.FormValue("m"),
}
var err error
p.Pages, err = app.db.GetInstancePages()
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)}
}
// Add in default pages
var hasAbout, hasPrivacy bool
for i, c := range p.Pages {
if hasAbout && hasPrivacy {
break
}
if c.ID == "about" {
hasAbout = true
if !c.Title.Valid {
p.Pages[i].Title = defaultAboutTitle(app.cfg)
}
} else if c.ID == "privacy" {
hasPrivacy = true
if !c.Title.Valid {
p.Pages[i].Title = defaultPrivacyTitle()
}
}
}
if !hasAbout {
p.Pages = append(p.Pages, &instanceContent{
ID: "about",
Title: defaultAboutTitle(app.cfg),
Content: defaultAboutPage(app.cfg),
Updated: defaultPageUpdatedTime,
})
}
if !hasPrivacy {
p.Pages = append(p.Pages, &instanceContent{
ID: "privacy",
Title: defaultPrivacyTitle(),
Content: defaultPrivacyPolicy(app.cfg),
Updated: defaultPageUpdatedTime,
})
}
showUserPage(w, "pages", p)
return nil
}
func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
slug := vars["slug"]
if slug == "" {
return impart.HTTPError{http.StatusFound, "/admin/pages"}
}
p := struct {
*UserPage
Config config.AppCfg
Message string
Content *instanceContent
}{
Config: app.cfg.App,
Message: r.FormValue("m"),
}
var err error
// Get pre-defined pages, or select slug
if slug == "about" {
p.Content, err = getAboutPage(app)
} else if slug == "privacy" {
p.Content, err = getPrivacyPage(app)
} else {
p.Content, err = app.db.GetDynamicContent(slug)
}
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
}
title := "New page"
if p.Content != nil {
title = "Edit " + p.Content.ID
} else {
p.Content = &instanceContent{}
}
p.UserPage = NewUserPage(app, r, u, title, nil)
showUserPage(w, "view-page", p)
return nil
}
func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["page"]
......@@ -235,11 +340,11 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req
// Update page
m := ""
err := app.db.UpdateDynamicContent(id, r.FormValue("content"))
err := app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
if err != nil {
m = "?m=" + err.Error()
}
return impart.HTTPError{http.StatusFound, "/admin" + m + "#page-" + id}
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
}
func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
......
......@@ -54,7 +54,7 @@ var (
debugging bool
// Software version can be set from git env using -ldflags
softwareVer = "0.8.1"
softwareVer = "0.9.0"
// DEPRECATED VARS
// TODO: pass app.cfg into GetCollection* calls so we can get these values
......@@ -115,6 +115,7 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
p := struct {
page.StaticPage
ContentTitle string
Content template.HTML
PlainContent string
Updated string
......@@ -124,8 +125,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
StaticPage: pageForReq(app, r),
}
if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
var c string
var updated *time.Time
var c *instanceContent
var err error
if r.URL.Path == "/about" {
......@@ -136,16 +136,17 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
} else {
c, updated, err = getPrivacyPage(app)
c, err = getPrivacyPage(app)
}
if err != nil {
return err
}
p.Content = template.HTML(applyMarkdown([]byte(c), ""))
p.PlainContent = shortPostDescription(stripmd.Strip(c))
if updated != nil {
p.Updated = updated.Format("January 2, 2006")
p.ContentTitle = c.Title.String
p.Content = template.HTML(applyMarkdown([]byte(c.Content), ""))
p.PlainContent = shortPostDescription(stripmd.Strip(c.Content))
if !c.Updated.IsZero() {
p.Updated = c.Updated.Format("January 2, 2006")
}
}
......@@ -636,12 +637,18 @@ func adminInitDatabase(app *app) error {
}
// Set up migrations table
log.Info("Updating appmigrations table...")
log.Info("Initializing appmigrations table...")
err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil {
return fmt.Errorf("Unable to set initial migrations: %v", err)
}
log.Info("Running migrations...")
err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil {
return fmt.Errorf("migrate: %s", err)
}
log.Info("Done.")
return nil
}
......@@ -112,8 +112,7 @@ func IsValidUsername(cfg *config.Config, username string) bool {
// Username is invalid if page with the same name exists. So traverse
// available pages, adding them to reservedUsernames map that'll be checked
// later.
// TODO: use pagesDir const
filepath.Walk("pages/", func(path string, i os.FileInfo, err error) error {
filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error {
reservedUsernames[i.Name()] = true
return nil
})
......
......@@ -11,7 +11,7 @@ host = db
port = 3306
[app]
site_name = Write Freely Example Blog!
site_name = WriteFreely Example Blog!
host = http://localhost:8080
theme = write
disable_js = false
......
......@@ -47,7 +47,7 @@ func Configure(fname string) (*SetupData, error) {
intro := color.New(color.Bold, color.FgWhite).PrintlnFunc()
fmt.Println()
intro(" ✍ Write Freely Configuration ✍")
intro(" ✍ WriteFreely Configuration ✍")
fmt.Println()
fmt.Println(wordwrap.WrapString(" This quick configuration process will "+action+" the application's config file, "+fname+".\n\n It validates your input along the way, so you can be sure any future errors aren't caused by a bad configuration. If you'd rather configure your server manually, instead run: writefreely --create-config and edit that file.", 75))
fmt.Println()
......
......@@ -115,8 +115,8 @@ type writestore interface {
GetUsersInvitedCount(id string) int64
CreateInvitedUser(inviteID string, userID int64) error
GetDynamicContent(id string) (string, *time.Time, error)
UpdateDynamicContent(id, content string) error
GetDynamicContent(id string) (*instanceContent, error)
UpdateDynamicContent(id, title, content, contentType string) error
GetAllUsers(page uint) (*[]User, error)
GetAllUsersCount() int64
GetUserLastPostTime(id int64) (*time.Time, error)
......@@ -2234,7 +2234,7 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) {
err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive)
switch {
case err == sql.ErrNoRows:
return nil, nil
return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."}
case err != nil:
log.Error("Failed selecting invite: %v", err)
return nil, err
......@@ -2262,26 +2262,63 @@ func (db *datastore) CreateInvitedUser(inviteID string, userID int64) error {
return err
}
func (db *datastore) GetDynamicContent(id string) (string, *time.Time, error) {
var c string
var u *time.Time
err := db.QueryRow("SELECT content, updated FROM appcontent WHERE id = ?", id).Scan(&c, &u)
func (db *datastore) GetInstancePages() ([]*instanceContent, error) {
return db.GetAllDynamicContent("page")
}
func (db *datastore) GetAllDynamicContent(t string) ([]*instanceContent, error) {
where := ""
params := []interface{}{}
if t != "" {
where = " WHERE content_type = ?"
params = append(params, t)
}
rows, err := db.Query("SELECT id, title, content, updated, content_type FROM appcontent"+where, params...)
if err != nil {
log.Error("Failed selecting from appcontent: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve instance pages."}
}
defer rows.Close()
pages := []*instanceContent{}
for rows.Next() {
c := &instanceContent{}
err = rows.Scan(&c.ID, &c.Title, &c.Content, &c.Updated, &c.Type)
if err != nil {
log.Error("Failed scanning row: %v", err)
break
}
pages = append(pages, c)
}
err = rows.Err()
if err != nil {
log.Error("Error after Next() on rows: %v", err)
}
return pages, nil
}
func (db *datastore) GetDynamicContent(id string) (*instanceContent, error) {
c := &instanceContent{
ID: id,
}
err := db.QueryRow("SELECT title, content, updated, content_type FROM appcontent WHERE id = ?", id).Scan(&c.Title, &c.Content, &c.Updated, &c.Type)
switch {
case err == sql.ErrNoRows:
return "", nil, nil
return nil, nil
case err != nil:
log.Error("Couldn't SELECT FROM appcontent for id '%s': %v", id, err)
return "", nil, err
return nil, err
}
return c, u, nil
return c, nil
}
func (db *datastore) UpdateDynamicContent(id, content string) error {
func (db *datastore) UpdateDynamicContent(id, title, content, contentType string) error {
var err error
if db.driverName == driverSQLite {
_, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+")", id, content)
_, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?)", id, title, content, contentType)
} else {
_, err = db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+") "+db.upsert("id")+" content = ?, updated = "+db.now(), id, content, content)
_, err = db.Exec("INSERT INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?) "+db.upsert("id")+" title = ?, content = ?, updated = "+db.now(), id, title, content, contentType, title, content)
}
if err != nil {
log.Error("Unable to INSERT appcontent for '%s': %v", id, err)
......
......@@ -62,7 +62,7 @@ require (
github.com/writeas/saturday v1.7.1
github.com/writeas/slug v1.2.0
github.com/writeas/web-core v1.0.0
github.com/writefreely/go-nodeinfo v1.1.0
github.com/writefreely/go-nodeinfo v1.2.0
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f // indirect
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect
......
......@@ -164,6 +164,8 @@ github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ
github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE=
github.com/writefreely/go-nodeinfo v1.1.0 h1:dp/ieEu0/gTeNKFvJTYhzBBouyFn7aiWtWzkb8J1JLg=
github.com/writefreely/go-nodeinfo v1.1.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f h1:ETU2VEl7TnT5bl7IvuKEzTDpplg5wzGYsOCAPhdoEIg=
......
......@@ -9,6 +9,19 @@ header.admin {
margin-left: 1em;
}
}
nav#admin {
display: block;
margin: 0.5em 0;
a {
color: @primary;
&:first-child {
margin-left: 0;
}
&.selected {
font-weight: bold;
}
}
}
.pager {
display: flex;
justify-content: center;
......
......@@ -46,6 +46,11 @@ body {
}
}
blockquote {
p + p {
margin: -2em 0 0.5em;
}
}
article {
margin-bottom: 2em !important;
......
......@@ -47,6 +47,13 @@ func (db *datastore) typeChar(l int) string {
return fmt.Sprintf("CHAR(%d)", l)
}
func (db *datastore) typeVarChar(l int) string {
if db.driverName == driverSQLite {
return "TEXT"
}
return fmt.Sprintf("VARCHAR(%d)", l)
}
func (db *datastore) typeBool() string {
if db.driverName == driverSQLite {
return "INTEGER"
......@@ -58,6 +65,13 @@ func (db *datastore) typeDateTime() string {
return "DATETIME"
}
func (db *datastore) collateMultiByte() string {
if db.driverName == driverSQLite {
return ""
}
return " COLLATE utf8_bin"
}
func (db *datastore) engine() string {
if db.driverName == driverSQLite {
return ""
......
......@@ -55,7 +55,8 @@ func (m *migration) Migrate(db *datastore) error {
}
var migrations = []Migration{
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
}
// CurrentVer returns the current migration version the application is on
......@@ -64,7 +65,8 @@ func CurrentVer() int {
}
func SetInitialMigrations(db *datastore) error {
_, err := db.Exec("INSERT INTO appmigrations (version, migrated, result) VALUES (?, "+db.now()+", ?)", CurrentVer(), "")
// Included schema files represent changes up to V1, so note that in the database
_, err := db.Exec("INSERT INTO appmigrations (version, migrated, result) VALUES (?, "+db.now()+", ?)", 1, "")
if err != nil {
return err
}
......
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package migrations
func supportInstancePages(db *datastore) error {
t, err := db.Begin()
_, err = t.Exec(`ALTER TABLE appcontent ADD COLUMN title ` + db.typeVarChar(255) + db.collateMultiByte() + ` NULL`)
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`ALTER TABLE appcontent ADD COLUMN content_type ` + db.typeVarChar(36) + ` DEFAULT 'page' NOT NULL`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}
......@@ -50,6 +50,7 @@ func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config {
},
MaxBlogs: cfg.App.MaxBlogs,
PublicReader: cfg.App.LocalTimeline,
Invites: cfg.App.UserInvites != "",
},
Protocols: []nodeinfo.NodeProtocol{
nodeinfo.ProtocolActivityPub,
......
......@@ -37,3 +37,8 @@ func (sp *StaticPage) SanitizeHost(cfg *config.Config) {
sp.Host = cfg.Server.HiddenHost
}
}
func (sp StaticPage) OfficialVersion() string {
p := strings.Split(sp.Version, "-")
return p[0]
}
......@@ -11,39 +11,71 @@
package writefreely
import (
"database/sql"
"github.com/writeas/writefreely/config"
"time"
)
func getAboutPage(app *app) (string, error) {
c, _, err := app.db.GetDynamicContent("about")
var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local)
func getAboutPage(app *app) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("about")
if err != nil {
return "", err
return nil, err
}
if c == "" {
if app.cfg.App.Federation {
c = `_` + app.cfg.App.SiteName + `_ is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub.`
} else {
c = `_` + app.cfg.App.SiteName + `_ is a place for you to write and publish, powered by WriteFreely.`
if c == nil {
c = &instanceContent{
ID: "about",
Type: "page",
Content: defaultAboutPage(app.cfg),
}
}
if !c.Title.Valid {
c.Title = defaultAboutTitle(app.cfg)
}
return c, nil
}
func getPrivacyPage(app *app) (string, *time.Time, error) {
c, updated, err := app.db.GetDynamicContent("privacy")
func defaultAboutTitle(cfg *config.Config) sql.NullString {
return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true}
}
func getPrivacyPage(app *app) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("privacy")
if err != nil {
return "", nil, err
return nil, err
}
if c == nil {
c = &instanceContent{