WebCalendar

Running WordPress? There’s now a WordPress-native version — WebCalendar for WordPress — that installs as a plugin instead of a separate PHP application, with full recurring-event support, iCal import/export, and a built-in holiday library. It’s free on WordPress.org, and there’s an overview here on k5n.us. The standalone PHP application on this page is still maintained and is not going away.

About WebCalendar

WebCalendar is a PHP-based calendar application that can be configured as a single-user calendar, a multi-user calendar for groups of users, or as an event calendar viewable by visitors. MySQL/MariaDB, SQLite3, PostgreSQL, Oracle, DB2, Interbase, MS SQL Server, or ODBC is required. The version 1.9.X releases are still a little rough around the edges since these include an overhaul of the UI to use Bootstrap and jQuery and a complete rewrite of the web-based installer.

WebCalendar can be setup in a variety of ways, such as…

  • A schedule management system for a single person
  • A schedule management system for a group of people, allowing one or more assistants to manage the calendar of another user
  • An events schedule that anyone can view, allowing visitors to submit new events
  • A calendar server that can be viewed with iCalendar-compliant calendar applications like Mozilla Sunbird, Apple iCal or GNOME Evolution or RSS-enabled applications like Firefox, Thunderbird, RSSOwl, FeedDemon, or BlogExpress.

Overview of Features

  • Multi-user support
  • 30 supported languages: Basque, Bulgarian, Chinese-Big5, Chinese-GB2312, Czech, Danish, Dutch, English-US, Estonian, Finnish, French, Galician, German, Greek, Holo-Big5, Hungarian, Icelandic, Italian, Japanese, Korean, Norwegian, Polish, Portuguese_BR, Portuguese, Romanian, Russian, Spanish, Swedish, Turkish, Welsh (see current list of translations here)
  • Web-based installer
  • Auto-detect user’s language preference from browser settings
  • View calendars by day, week, month or year
  • View another user’s calendar
  • View one or more users’ calendar via layers on top of your own calendar
  • Add/Edit/Delete users
  • Add/Edit/Delete events
  • Repeating events including support for overriding or deleting (exceptions)
  • Configurable custom event fields
  • User-configurable preferences for colors, 12/24 time format, Sun/Mon week start
  • Checks for scheduling conflicts
  • Email reminders for upcoming events
  • Email notifications for new/updated/deleted events
  • Export events to iCalendar
  • Import from iCalendar/ics format
  • Optional general access (no login required) to allow calendar to be viewed by people without a login (useful for event calendars)
  • Users can make their calendar available publicly to anyone with an iCalendar-compliant calendar program (such as Apple’s iCal, Mozilla Calendar or Sunbird)
  • Publishing of free/busy schedules (part of the iCalendar standard)
  • RSS support that puts a user’s calendar into RSS
  • Subscribe to “remote” calendars (hosted elsewhere on the net) in either iCalendar or hCalendar formats (WebCalendar 1.1+)
  • User authentication: Web-based, HTTP, LDAP or NIS

System Requirements

  • PHP 8 or later
  • PHP support and access to one of the following databases:
    • SQLite
    • MySQL/MariaDB
    • Oracle
    • Postgres
    • IBM DB2
  • Access to cron for Linux/Unix systems (to send out reminders)

Development Cost

The following metrics from Ohloh show how much it would have cost to commercially develop WebCalendar.

  • Codebase Size: 138,588 lines
  • Estimated Effort: 34 person-years
  • Estimated Cost: $1,884,469
  • (As of 11 August 2024)

Donations

If you’d like to help support the costs of developing, maintaining and supporting WebCalendar, please consider donating.

Developer Resources

License

WebCalendar is available under the GNU General Public License, version 2.

For more information on this license:

Documentation

Most Recent Changes

Below are the most recent source code commits to github on the master branch.

  • Elvis and other PHP short form operators (#520)
    by bbannon on September 13, 2026 at 10:31 pm

    Elvis and other PHP short form operators (#520) Shorten access.php using PHP short-form operators: `?:`, `??` and the null-coalescing assignment, plus tidier formatting of the webcal_access_user INSERT. The three-level permission cascade (global default, wider default, specific pair) is rewritten as a list of levels applied in order — shorter than the original while keeping all three levels, verified equivalent over every combination of present/absent/empty in both roles. `strlen()` is kept rather than `?:` on the POST values, since `?:` would turn a literal ‘0’ into ‘N’, and the locals are cast to string because getPostValue() returns NULL for a missing field (strlen(null) and trim(null) are deprecated on PHP 8.1+). Reading $ALLOW_VIEW_OTHER is shortened to a strict comparison, which is identical to the previous `! empty() && == ‘Y’` for every input. It is always defined since #734. Co-authored-by: Bruce Bannon <github.ful9l@simplelogin.com>

  • Merge pull request #735 from craigk5n/fix/config-defaults-734
    by craigk5n on September 13, 2026 at 10:00 pm

    Merge pull request #735 from craigk5n/fix/config-defaults-734 fix: resolve config settings to their documented defaults (#734)

  • fix: select_db before the mysqli config lookup (#734)
    by craigk5n on September 13, 2026 at 9:45 pm

    fix: select_db before the mysqli config lookup (#734) The MySQL Selenium install failed in CI while PostgreSQL and SQLite passed. Two defects in the seeding added by the previous commit: connectMysqli() opens the connection with no database argument, so every mysqli query in WizardDatabase selects the database first — lines 160, 232, 322, 355, 395 and 430 all do it. getExistingConfigSettings() did not. The SELECT therefore failed, “@” swallowed it, the lookup returned an empty set, and seeding tried to re-insert all ~160 rows into a table that already had them. isIgnorableSchemaError() then did not rescue it: it covers duplicate DDL (“Duplicate column name”, “Duplicate key name”) but not MySQL’s duplicate-row error, so the first re-insert aborted executeUpgrade() and took the install down with it. PostgreSQL has no select_db concept and the SQLite lookup read the table fine, which is why only MySQL broke. Fix the root cause, and stop a blind lookup from being fatal: seeding only ever means “make sure this row exists”, so a duplicate-row error is the desired end state rather than a reason to abort an upgrade. A real error still aborts. Result handles are freed on all three drivers. Tests take the duplicate wording from the driver rather than asserting a hand-written string, so they fail if the pattern stops matching reality; the MySQL and PostgreSQL wordings cannot be produced locally and are pinned as strings. 715 tests, PHPStan clean, 278 files compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdx7ZiULx7Xf2xncNJenxh

  • fix: resolve config settings to their documented defaults (#734)
    by craigk5n on September 13, 2026 at 9:20 pm

    fix: resolve config settings to their documented defaults (#734) A setting’s default was defined in three places that did not agree, and which one won depended on whether the site was installed or upgraded. includes/default_config.php documents a default for every setting, but the wizard only wrote those rows into webcal_config when the database was empty (WizardDatabase::executeUpgrade gated loadDefaultConfig() on databaseIsEmpty), and wizard/shared/upgrade-sql.php contains no INSERT INTO webcal_config at all. load_global_settings() then read that table and nothing else, so on an upgraded site any setting added since the original install had no row and its global stayed undefined — leaving each of ~160 call sites to decide for itself what “unset” meant. They disagreed, and several guessed permissively. Resolve it at the source instead of per call site: – load_global_settings() now falls back to the documented default for any setting with no row. It runs last so the request- and language-derived values above it (TIMEZONE, APPLICATION_NAME, FONTS — Japanese prepends Osaka) still win, and uses isset() rather than empty() so an admin’s deliberately blank value is preserved. – The wizard seeds defaults on upgrades too, skipping settings that already have a row so an admin’s choices are never overwritten. – admin.php’s save_pref() stored a cleared field by deleting the row, which is not the same as an empty value; it now writes ” back. – default_config.php exposes webcal_config_defaults(). `require` inside a function makes $webcalConfig a local of that function, so callers that cannot control the require scope could not reach it. $webcalConfig is still assigned for existing callers. Nine settings admin.php exposes had no documented default at all and were undefined on every install: DEFAULT_VISIBILITY, HOME_LINK, MCP_CORS_CUSTOM, MCP_CORS_ORIGINS, MCP_RATE_LIMIT, MCP_SERVER_ENABLED, MCP_WRITE_ACCESS, SECURITY_AUDIT_EXTRA_EXCLUDES, SECURITY_AUDIT_NOISE_FILTER. Each is set to the value its own call site already assumed, so behaviour is unchanged; DEFAULT_VISIBILITY is ‘P’ to match what edit_entry.php actually does, which also stops admin.php reporting ‘Private’ when new events are Public. Permission gates that read a setting with == ‘N’ or != ‘N’ treat an unset value as permission granted. The fallback keeps them defined, but a gate that is only correct because something else populated the global is one refactor from being wrong again, so they now compare against ‘Y’: availability.php (its only authorization check), the participant lists in build_entry_popup() and view_entry.php, and the six SEND_EMAIL sites. SERVER_URL is computed and inserted by load_global_settings(); since a cleared field now leaves an empty row rather than no row, that bare INSERT would silently fail on the primary key, so it deletes first. Verified: 710 tests (17 new), PHPStan clean, 278 files compile. Both new tests were confirmed to fail with the fix reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdx7ZiULx7Xf2xncNJenxh

  • Merge pull request #710 from bbannon/master
    by craigk5n on September 7, 2026 at 7:27 pm

    Merge pull request #710 from bbannon/master Master

Download Metrics

  • Downloads via Github: 21052
  • Downloads via SourceForge: 1417566

Related Links

  • Standards
    • RFC 2445: Internet Calendaring and Scheduling Core Object Specification (iCalendar)
    • CalDAV: Calendaring and Scheduling Extensions to WebDAV (DRAFT) 
      [Note: WebCalendar does not yet support CalDAV.)
  • Calendar client applications – You can use the applications to view events stored in WebCalendar if you enable its publishing settings.
  • iCalendar/ics download sites – These sites contain calendars for holidays, sports teams schedules, music converts, etc. You can import these files into WebCalendar.